1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-22 22:59:39 +02:00

Add nice formatting for subtypes on account list (#2138)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions

* Add nice formatting for subtypes on account list

* Fix rubocop linting errors

* Implement better mapping

* Fix rubocop linting

* Add short and long versions of subtypes

* Simplify subtype reference

Co-authored-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>
Signed-off-by: Alex Hatzenbuhler <hatz@hey.com>

* Simplify reference logic, add a small test

* Fix test

* Fix tests

---------

Signed-off-by: Alex Hatzenbuhler <hatz@hey.com>
Co-authored-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>
This commit is contained in:
Alex Hatzenbuhler 2025-04-22 13:10:50 -05:00 committed by GitHub
parent db34f6d7a2
commit 47aeaf8cea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 83 additions and 30 deletions

View file

@ -155,6 +155,16 @@ class Account < ApplicationRecord
first_valuation&.amount_money || balance_money first_valuation&.amount_money || balance_money
end end
# Get short version of the subtype label
def short_subtype_label
accountable_class.short_subtype_label_for(subtype) || accountable_class.display_name
end
# Get long version of the subtype label
def long_subtype_label
accountable_class.long_subtype_label_for(subtype) || accountable_class.display_name
end
private private
def sync_balances def sync_balances
strategy = linked? ? :reverse : :forward strategy = linked? ? :reverse : :forward

View file

@ -59,6 +59,7 @@ class BalanceSheet
account.define_singleton_method(:weight) do account.define_singleton_method(:weight) do
classification_total.zero? ? 0 : account.converted_balance / classification_total.to_d * 100 classification_total.zero? ? 0 : account.converted_balance / classification_total.to_d * 100
end end
account account
end.sort_by(&:weight).reverse end.sort_by(&:weight).reverse
) )

View file

@ -3,6 +3,9 @@ module Accountable
TYPES = %w[Depository Investment Crypto Property Vehicle OtherAsset CreditCard Loan OtherLiability] TYPES = %w[Depository Investment Crypto Property Vehicle OtherAsset CreditCard Loan OtherLiability]
# Define empty hash to ensure all accountables have this defined
SUBTYPES = {}.freeze
def self.from_type(type) def self.from_type(type)
return nil unless TYPES.include?(type) return nil unless TYPES.include?(type)
type.constantize type.constantize
@ -27,6 +30,24 @@ module Accountable
raise NotImplementedError, "Accountable must implement #color" raise NotImplementedError, "Accountable must implement #color"
end end
# Given a subtype, look up the label for this accountable type
def subtype_label_for(subtype, format: :short)
return nil if subtype.nil?
label_type = format == :long ? :long : :short
self::SUBTYPES[subtype]&.fetch(label_type, nil)
end
# Convenience method for getting the short label
def short_subtype_label_for(subtype)
subtype_label_for(subtype, format: :short)
end
# Convenience method for getting the long label
def long_subtype_label_for(subtype)
subtype_label_for(subtype, format: :long)
end
def favorable_direction def favorable_direction
classification == "asset" ? "up" : "down" classification == "asset" ? "up" : "down"
end end

View file

@ -1,10 +1,10 @@
class Depository < ApplicationRecord class Depository < ApplicationRecord
include Accountable include Accountable
SUBTYPES = [ SUBTYPES = {
[ "Checking", "checking" ], "checking" => { short: "Checking", long: "Checking" },
[ "Savings", "savings" ] "savings" => { short: "Savings", long: "Savings" }
].freeze }.freeze
class << self class << self
def display_name def display_name

View file

@ -1,20 +1,20 @@
class Investment < ApplicationRecord class Investment < ApplicationRecord
include Accountable include Accountable
SUBTYPES = [ SUBTYPES = {
[ "Brokerage", "brokerage" ], "brokerage" => { short: "Brokerage", long: "Brokerage" },
[ "Pension", "pension" ], "pension" => { short: "Pension", long: "Pension" },
[ "Retirement", "retirement" ], "retirement" => { short: "Retirement", long: "Retirement" },
[ "401(k)", "401k" ], "401k" => { short: "401(k)", long: "401(k)" },
[ "Traditional 401(k)", "traditional_401k" ], "traditional_401k" => { short: "Traditional 401(k)", long: "Traditional 401(k)" },
[ "Roth 401(k)", "roth_401k" ], "roth_401k" => { short: "Roth 401(k)", long: "Roth 401(k)" },
[ "529 Plan", "529_plan" ], "529_plan" => { short: "529 Plan", long: "529 Plan" },
[ "Health Savings Account", "hsa" ], "hsa" => { short: "HSA", long: "Health Savings Account" },
[ "Mutual Fund", "mutual_fund" ], "mutual_fund" => { short: "Mutual Fund", long: "Mutual Fund" },
[ "Traditional IRA", "traditional_ira" ], "traditional_ira" => { short: "Traditional IRA", long: "Traditional IRA" },
[ "Roth IRA", "roth_ira" ], "roth_ira" => { short: "Roth IRA", long: "Roth IRA" },
[ "Angel", "angel" ] "angel" => { short: "Angel", long: "Angel" }
].freeze }.freeze
class << self class << self
def color def color

View file

@ -1,14 +1,14 @@
class Property < ApplicationRecord class Property < ApplicationRecord
include Accountable include Accountable
SUBTYPES = [ SUBTYPES = {
[ "Single Family Home", "single_family_home" ], "single_family_home" => { short: "Single Family Home", long: "Single Family Home" },
[ "Multi-Family Home", "multi_family_home" ], "multi_family_home" => { short: "Multi-Family Home", long: "Multi-Family Home" },
[ "Condominium", "condominium" ], "condominium" => { short: "Condo", long: "Condominium" },
[ "Townhouse", "townhouse" ], "townhouse" => { short: "Townhouse", long: "Townhouse" },
[ "Investment Property", "investment_property" ], "investment_property" => { short: "Investment Property", long: "Investment Property" },
[ "Second Home", "second_home" ] "second_home" => { short: "Second Home", long: "Second Home" }
] }.freeze
has_one :address, as: :addressable, dependent: :destroy has_one :address, as: :addressable, dependent: :destroy

View file

@ -17,6 +17,9 @@
</p> </p>
<% else %> <% else %>
<%= link_to account.name, account, class: [(account.is_active ? "text-primary" : "text-subdued"), "text-sm font-medium hover:underline"], data: { turbo_frame: "_top" } %> <%= link_to account.name, account, class: [(account.is_active ? "text-primary" : "text-subdued"), "text-sm font-medium hover:underline"], data: { turbo_frame: "_top" } %>
<% if account.long_subtype_label %>
<p class="text-sm text-secondary truncate"><%= account.long_subtype_label %></p>
<% end %>
<% end %> <% end %>
</div> </div>

View file

@ -24,7 +24,7 @@
<div class="min-w-0 grow"> <div class="min-w-0 grow">
<%= tag.p account.name, class: "text-sm text-primary font-medium mb-0.5 truncate" %> <%= tag.p account.name, class: "text-sm text-primary font-medium mb-0.5 truncate" %>
<%= tag.p account.subtype&.humanize.presence || account_group.name, class: "text-sm text-secondary truncate" %> <%= tag.p account.short_subtype_label, class: "text-sm text-secondary truncate" %>
</div> </div>
<div class="ml-auto text-right grow h-10"> <div class="ml-auto text-right grow h-10">

View file

@ -2,6 +2,6 @@
<%= render "accounts/form", account: account, url: url do |form| %> <%= render "accounts/form", account: account, url: url do |form| %>
<%= form.select :subtype, <%= form.select :subtype,
Depository::SUBTYPES, Depository::SUBTYPES.map { |k, v| [v[:long], k] },
{ label: true, prompt: t("depositories.form.subtype_prompt"), include_blank: t("depositories.form.none") } %> { label: true, prompt: t("depositories.form.subtype_prompt"), include_blank: t("depositories.form.none") } %>
<% end %> <% end %>

View file

@ -2,6 +2,6 @@
<%= render "accounts/form", account: account, url: url do |form| %> <%= render "accounts/form", account: account, url: url do |form| %>
<%= form.select :subtype, <%= form.select :subtype,
Investment::SUBTYPES, Investment::SUBTYPES.map { |k, v| [v[:long], k] },
{ label: true, prompt: t("investments.form.subtype_prompt"), include_blank: t("investments.form.none") } %> { label: true, prompt: t("investments.form.subtype_prompt"), include_blank: t("investments.form.none") } %>
<% end %> <% end %>

View file

@ -2,7 +2,7 @@
<%= render "accounts/form", account: account, url: url do |form| %> <%= render "accounts/form", account: account, url: url do |form| %>
<%= form.select :subtype, <%= form.select :subtype,
Property::SUBTYPES, Property::SUBTYPES.map { |k, v| [v[:long], k] },
{ label: true, prompt: t("properties.form.subtype_prompt"), include_blank: t("properties.form.none") } %> { label: true, prompt: t("properties.form.subtype_prompt"), include_blank: t("properties.form.none") } %>
<hr class="my-4"> <hr class="my-4">

View file

@ -13,4 +13,22 @@ class AccountTest < ActiveSupport::TestCase
@account.destroy @account.destroy
end end
end end
test "gets short/long subtype label" do
account = @family.accounts.create!(
name: "Test Investment",
balance: 1000,
currency: "USD",
subtype: "hsa",
accountable: Investment.new
)
assert_equal "HSA", account.short_subtype_label
assert_equal "Health Savings Account", account.long_subtype_label
# Test with nil subtype
account.update!(subtype: nil)
assert_equal "Investments", account.short_subtype_label
assert_equal "Investments", account.long_subtype_label
end
end end