mirror of
https://github.com/we-promise/sure.git
synced 2026-08-03 23:52:13 +00:00
Include uncategorized spending in budget UI (#2877)
This commit is contained in:
@@ -2,7 +2,7 @@ module BudgetsHelper
|
||||
def budget_has_over_budget?(budget)
|
||||
return false unless budget.initialized?
|
||||
|
||||
budget.budget_categories.any?(&:any_over_budget?)
|
||||
budget.budget_categories.any?(&:any_over_budget?) || budget.uncategorized_budget_category.any_over_budget?
|
||||
end
|
||||
|
||||
def budget_categories_view_state(budget)
|
||||
|
||||
@@ -208,7 +208,7 @@ class Budget < ApplicationRecord
|
||||
# Continuous gray segment for empty budgets
|
||||
return [ { color: "var(--budget-unallocated-fill)", amount: 1, id: unused_segment_id } ] unless allocations_valid?
|
||||
|
||||
segments = budget_categories.reject(&:subcategory?).map do |bc|
|
||||
segments = donut_budget_categories.map do |bc|
|
||||
{ color: bc.category.color, amount: budget_category_actual_spending(bc), id: bc.id }
|
||||
end
|
||||
|
||||
@@ -219,6 +219,17 @@ class Budget < ApplicationRecord
|
||||
segments
|
||||
end
|
||||
|
||||
def donut_budget_categories
|
||||
categories = budget_categories.reject(&:subcategory?).to_a
|
||||
uncategorized = uncategorized_budget_category
|
||||
|
||||
if budget_category_actual_spending(uncategorized).positive?
|
||||
categories << uncategorized
|
||||
end
|
||||
|
||||
categories
|
||||
end
|
||||
|
||||
# =============================================================================
|
||||
# Actuals: How much user has spent on each budget category
|
||||
# =============================================================================
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% budget.budget_categories.each do |bc| %>
|
||||
<% budget.donut_budget_categories.each do |bc| %>
|
||||
<div id="segment_<%= bc.id %>" class="hidden">
|
||||
<div class="flex flex-col gap-2 items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
|
||||
@@ -104,4 +104,49 @@ class BudgetsHelperTest < ActionView::TestCase
|
||||
assert_equal [ child.id ], group.budget_subcategories.map(&:category_id)
|
||||
assert group.budget_subcategories.first.any_over_budget?
|
||||
end
|
||||
|
||||
test "budget has over budget when uncategorized spending exceeds its allocation" do
|
||||
family = Family.create!(name: "Helper Budget Repro", currency: "USD")
|
||||
account = Account.create!(
|
||||
family: family,
|
||||
accountable: Depository.new,
|
||||
name: "Checking",
|
||||
status: "active",
|
||||
currency: "USD",
|
||||
balance: 0
|
||||
)
|
||||
|
||||
category = Category.create!(
|
||||
name: "Helper Groceries #{SecureRandom.hex(4)}",
|
||||
family: family,
|
||||
color: "#407706",
|
||||
lucide_icon: "shopping-bag"
|
||||
)
|
||||
|
||||
budget = Budget.create!(
|
||||
family: family,
|
||||
start_date: Date.current.beginning_of_month,
|
||||
end_date: Date.current.end_of_month,
|
||||
currency: "USD",
|
||||
budgeted_spending: 100
|
||||
)
|
||||
|
||||
BudgetCategory.create!(
|
||||
budget: budget,
|
||||
category: category,
|
||||
budgeted_spending: 100,
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
Entry.create!(
|
||||
account: account,
|
||||
entryable: Transaction.create!(category: nil),
|
||||
date: Date.current,
|
||||
name: "Helper Uncategorized Over Budget",
|
||||
amount: 125,
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
assert budget_has_over_budget?(Budget.find(budget.id))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -263,6 +263,7 @@ class BudgetTest < ActiveSupport::TestCase
|
||||
budget.stubs(:available_to_spend).returns(200)
|
||||
budget.stubs(:budget_category_actual_spending).with(parent_budget_category).returns(63.11)
|
||||
budget.stubs(:budget_category_actual_spending).with(standalone_budget_category).returns(25)
|
||||
budget.stubs(:budget_category_actual_spending).with(budget.uncategorized_budget_category).returns(0)
|
||||
|
||||
segments = budget.to_donut_segments_json
|
||||
|
||||
@@ -280,6 +281,59 @@ class BudgetTest < ActiveSupport::TestCase
|
||||
assert_equal 200, segments_by_id["unused"][:amount]
|
||||
end
|
||||
|
||||
test "to_donut_segments_json includes uncategorized spending" do
|
||||
family = @family
|
||||
account = Account.create!(
|
||||
family: family,
|
||||
accountable: Depository.new,
|
||||
name: "Checking",
|
||||
status: "active",
|
||||
currency: "USD",
|
||||
balance: 0
|
||||
)
|
||||
|
||||
category = Category.create!(
|
||||
name: "Groceries #{Time.now.to_f}",
|
||||
family: family,
|
||||
color: "#407706",
|
||||
lucide_icon: "shopping-bag"
|
||||
)
|
||||
|
||||
budget = Budget.create!(
|
||||
family: family,
|
||||
start_date: Date.current.beginning_of_month,
|
||||
end_date: Date.current.end_of_month,
|
||||
currency: "USD",
|
||||
budgeted_spending: 100
|
||||
)
|
||||
|
||||
BudgetCategory.create!(
|
||||
budget: budget,
|
||||
category: category,
|
||||
budgeted_spending: 100,
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
Entry.create!(
|
||||
account: account,
|
||||
entryable: Transaction.create!(category: nil),
|
||||
date: Date.current,
|
||||
name: "Uncategorized donut spending",
|
||||
amount: 125,
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
budget = Budget.find(budget.id)
|
||||
uncategorized = budget.uncategorized_budget_category
|
||||
segments = budget.to_donut_segments_json
|
||||
uncategorized_segment = segments.find { |segment| segment[:id] == uncategorized.id }
|
||||
|
||||
assert_equal 125, budget.actual_spending
|
||||
assert_equal 125, uncategorized.actual_spending
|
||||
assert_not_nil uncategorized_segment
|
||||
assert_equal 125, uncategorized_segment[:amount]
|
||||
end
|
||||
|
||||
test "actual_spending subtracts uncategorized refunds" do
|
||||
family = families(:dylan_family)
|
||||
budget = Budget.find_or_bootstrap(family, start_date: Date.current.beginning_of_month)
|
||||
|
||||
54
test/views/budgets/budget_donut_view_test.rb
Normal file
54
test/views/budgets/budget_donut_view_test.rb
Normal file
@@ -0,0 +1,54 @@
|
||||
require "test_helper"
|
||||
|
||||
class BudgetDonutViewTest < ActionView::TestCase
|
||||
test "renders hover content for uncategorized donut segment" do
|
||||
family = Family.create!(name: "Budget Donut View Repro", currency: "USD")
|
||||
account = Account.create!(
|
||||
family: family,
|
||||
accountable: Depository.new,
|
||||
name: "Checking",
|
||||
status: "active",
|
||||
currency: "USD",
|
||||
balance: 0
|
||||
)
|
||||
|
||||
category = Category.create!(
|
||||
name: "Groceries #{SecureRandom.hex(4)}",
|
||||
family: family,
|
||||
color: "#407706",
|
||||
lucide_icon: "shopping-bag"
|
||||
)
|
||||
|
||||
budget = Budget.create!(
|
||||
family: family,
|
||||
start_date: Date.current.beginning_of_month,
|
||||
end_date: Date.current.end_of_month,
|
||||
currency: "USD",
|
||||
budgeted_spending: 100
|
||||
)
|
||||
|
||||
BudgetCategory.create!(
|
||||
budget: budget,
|
||||
category: category,
|
||||
budgeted_spending: 100,
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
Entry.create!(
|
||||
account: account,
|
||||
entryable: Transaction.create!(category: nil),
|
||||
date: Date.current,
|
||||
name: "Uncategorized hover spending",
|
||||
amount: 125,
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
budget = Budget.find(budget.id)
|
||||
uncategorized = budget.uncategorized_budget_category
|
||||
|
||||
html = render(partial: "budgets/budget_donut", locals: { budget: budget })
|
||||
|
||||
assert_includes html, "segment_#{uncategorized.id}"
|
||||
assert_includes html, uncategorized.category.display_name
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user