mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 05:09:38 +02:00
Since the very first 0.1.0-alpha.1 release, we've been moving quickly to add new features to the Maybe app. In doing so, some parts of the codebase have become outdated, unnecessary, or overly-complex as a natural result of this feature prioritization. Now that "core" Maybe is complete, we're moving into a second phase of development where we'll be working hard to improve the accuracy of existing features and build additional features on top of "core". This PR is a quick overhaul of the existing codebase aimed to: - Establish the brand new and simplified dashboard view (pictured above) - Establish and move towards the conventions introduced in Cursor rules and project design overview #1788 - Consolidate layouts and improve the performance of layout queries - Organize the core models of the Maybe domain (i.e. Account::Entry, Account::Transaction, etc.) and break out specific traits of each model into dedicated concerns for better readability - Remove stale / dead code from codebase - Remove overly complex code paths in favor of simpler ones
119 lines
3.1 KiB
Ruby
119 lines
3.1 KiB
Ruby
class BudgetCategory < ApplicationRecord
|
|
include Monetizable
|
|
|
|
belongs_to :budget
|
|
belongs_to :category
|
|
|
|
validates :budget_id, uniqueness: { scope: :category_id }
|
|
|
|
monetize :budgeted_spending, :available_to_spend, :avg_monthly_expense, :median_monthly_expense, :actual_spending
|
|
|
|
class Group
|
|
attr_reader :budget_category, :budget_subcategories
|
|
|
|
delegate :category, to: :budget_category
|
|
delegate :name, :color, to: :category
|
|
|
|
def self.for(budget_categories)
|
|
top_level_categories = budget_categories.select { |budget_category| budget_category.category.parent_id.nil? }
|
|
top_level_categories.map do |top_level_category|
|
|
subcategories = budget_categories.select { |bc| bc.category.parent_id == top_level_category.category_id && top_level_category.category_id.present? }
|
|
new(top_level_category, subcategories.sort_by { |subcategory| subcategory.category.name })
|
|
end.sort_by { |group| group.category.name }
|
|
end
|
|
|
|
def initialize(budget_category, budget_subcategories = [])
|
|
@budget_category = budget_category
|
|
@budget_subcategories = budget_subcategories
|
|
end
|
|
end
|
|
|
|
class << self
|
|
def uncategorized
|
|
new(
|
|
id: Digest::UUID.uuid_v5(Digest::UUID::URL_NAMESPACE, "uncategorized"),
|
|
category: nil,
|
|
)
|
|
end
|
|
end
|
|
|
|
def initialized?
|
|
budget.initialized?
|
|
end
|
|
|
|
def category
|
|
super || budget.family.categories.uncategorized
|
|
end
|
|
|
|
def name
|
|
category.name
|
|
end
|
|
|
|
def actual_spending
|
|
budget.budget_category_actual_spending(self)
|
|
end
|
|
|
|
def avg_monthly_expense
|
|
budget.category_avg_monthly_expense(category)
|
|
end
|
|
|
|
def median_monthly_expense
|
|
budget.category_median_monthly_expense(category)
|
|
end
|
|
|
|
def subcategory?
|
|
category.parent_id.present?
|
|
end
|
|
|
|
def available_to_spend
|
|
(budgeted_spending || 0) - actual_spending
|
|
end
|
|
|
|
def percent_of_budget_spent
|
|
return 0 unless budgeted_spending > 0
|
|
|
|
(actual_spending / budgeted_spending) * 100
|
|
end
|
|
|
|
def to_donut_segments_json
|
|
unused_segment_id = "unused"
|
|
overage_segment_id = "overage"
|
|
|
|
return [ { color: "#F0F0F0", amount: 1, id: unused_segment_id } ] unless actual_spending > 0
|
|
|
|
segments = [ { color: category.color, amount: actual_spending, id: id } ]
|
|
|
|
if available_to_spend.negative?
|
|
segments.push({ color: "#EF4444", amount: available_to_spend.abs, id: overage_segment_id })
|
|
else
|
|
segments.push({ color: "#F0F0F0", amount: available_to_spend, id: unused_segment_id })
|
|
end
|
|
|
|
segments
|
|
end
|
|
|
|
def siblings
|
|
budget.budget_categories.select { |bc| bc.category.parent_id == category.parent_id && bc.id != id }
|
|
end
|
|
|
|
def max_allocation
|
|
return nil unless subcategory?
|
|
|
|
parent_budget = budget.budget_categories.find { |bc| bc.category.id == category.parent_id }&.budgeted_spending
|
|
siblings_budget = siblings.sum(&:budgeted_spending)
|
|
|
|
[ parent_budget - siblings_budget, 0 ].max
|
|
end
|
|
|
|
def subcategories
|
|
return BudgetCategory.none unless category.parent_id.nil?
|
|
|
|
budget.budget_categories
|
|
.joins(:category)
|
|
.where(categories: { parent_id: category.id })
|
|
end
|
|
|
|
def subcategory?
|
|
category.parent_id.present?
|
|
end
|
|
end
|