Add customizable menu order for user accounts (#44)

* Add customizable menu order for user accounts

Introduces a MenuOrder model and concern to allow users to select their preferred account ordering (by name or balance, ascending or descending). Adds a default_order field to users, updates user preferences UI, and applies the selected order to balance sheet account listings.

* Rename MenuOrder to AccountOrder and update user order field

Refactors the MenuOrder model to AccountOrder and updates all references accordingly. Replaces the user's default_order field with default_account_order, including migration changes, validations, and form fields. Updates localization and schema to reflect the new naming.

* Update balance_sheet.rb

* Fix for nil Current.user when rake runs in balance_sheet model

---------

Signed-off-by: Aluisio Pereira <oaluiser@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
This commit is contained in:
Aluisio Pereira
2025-08-11 17:58:44 -03:00
committed by GitHub
parent dd0cb60b56
commit 6d4a5dd743
9 changed files with 114 additions and 5 deletions

View File

@@ -0,0 +1,14 @@
module Orderable
extend ActiveSupport::Concern
included do
before_action :set_order
end
private
def set_order
@order = AccountOrder.find(params[:order] || Current.user&.default_account_order)
rescue ArgumentError
@order = AccountOrder.default
end
end

View File

@@ -88,7 +88,7 @@ class UsersController < ApplicationController
def user_params
params.require(:user).permit(
:first_name, :last_name, :email, :profile_image, :redirect_to, :delete_profile_image, :onboarded_at,
:show_sidebar, :default_period, :show_ai_sidebar, :ai_enabled, :theme, :set_onboarding_preferences_at, :set_onboarding_goals_at,
:show_sidebar, :default_period, :default_account_order, :show_ai_sidebar, :ai_enabled, :theme, :set_onboarding_preferences_at, :set_onboarding_goals_at,
family_attributes: [ :name, :currency, :country, :locale, :date_format, :timezone, :id ],
goals: []
)

View File

@@ -0,0 +1,60 @@
class AccountOrder
include ActiveModel::Model
include ActiveModel::Attributes
ORDERS = {
"name_asc" => {
label: "Name (A-Z)",
label_short: "Name ↑",
sql_order: "name ASC"
},
"name_desc" => {
label: "Name (Z-A)",
label_short: "Name ↓",
sql_order: "name DESC"
},
"balance_asc" => {
label: "Balance (Low to High)",
label_short: "Balance ↑",
sql_order: "balance ASC"
},
"balance_desc" => {
label: "Balance (High to Low)",
label_short: "Balance ↓",
sql_order: "balance DESC"
}
}.freeze
attr_accessor :key
def initialize(key)
@key = key.to_s
raise ArgumentError, "Invalid order key: #{@key}" unless ORDERS.key?(@key)
end
def label
ORDERS.dig(key, :label)
end
def label_short
ORDERS.dig(key, :label_short)
end
def sql_order
ORDERS.dig(key, :sql_order)
end
class << self
def all
ORDERS.keys.map { |key| new(key) }
end
def find(key)
new(key) if ORDERS.key?(key.to_s)
end
def default
new("name_asc")
end
end
end

View File

@@ -13,7 +13,7 @@ class BalanceSheet
@assets ||= ClassificationGroup.new(
classification: "asset",
currency: family.currency,
accounts: account_totals.asset_accounts.sort_by(&:name)
accounts: sorted(account_totals.asset_accounts)
)
end
@@ -21,7 +21,7 @@ class BalanceSheet
@liabilities ||= ClassificationGroup.new(
classification: "liability",
currency: family.currency,
accounts: account_totals.liability_accounts.sort_by(&:name)
accounts: sorted(account_totals.liability_accounts)
)
end
@@ -61,4 +61,22 @@ class BalanceSheet
def net_worth_series_builder
@net_worth_series_builder ||= NetWorthSeriesBuilder.new(family)
end
def sorted(accounts)
account_order = Current.user&.account_order
order_key = account_order&.key || "name_asc"
case order_key
when "name_asc"
accounts.sort_by(&:name)
when "name_desc"
accounts.sort_by(&:name).reverse
when "balance_asc"
accounts.sort_by(&:balance)
when "balance_desc"
accounts.sort_by(&:balance).reverse
else
accounts
end
end
end

View File

@@ -15,6 +15,7 @@ class User < ApplicationRecord
validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validate :ensure_valid_profile_image
validates :default_period, inclusion: { in: Period::PERIODS.keys }
validates :default_account_order, inclusion: { in: AccountOrder::ORDERS.keys }
normalizes :email, with: ->(email) { email.strip.downcase }
normalizes :unconfirmed_email, with: ->(email) { email&.strip&.downcase }
@@ -163,6 +164,10 @@ class User < ApplicationRecord
!onboarded?
end
def account_order
AccountOrder.find(default_account_order) || AccountOrder.default
end
private
def ensure_valid_profile_image
return unless profile_image.attached?

View File

@@ -30,6 +30,11 @@
{ label: t(".default_period") },
{ data: { auto_submit_form_target: "auto" } } %>
<%= form.select :default_account_order,
AccountOrder.all.map { |order| [ order.label, order.key ] },
{ label: t(".default_account_order") },
{ data: { auto_submit_form_target: "auto" } } %>
<%= family_form.select :country,
country_options,
{ label: t(".country") },

View File

@@ -14,6 +14,7 @@ en:
general_subtitle: Configure your preferences
general_title: General
default_period: Default Period
default_account_order: Default Account Order
language: Language
page_title: Preferences
theme_dark: Dark

View File

@@ -0,0 +1,5 @@
class AddDefaultAccountOrderToUsers < ActiveRecord::Migration[7.2]
def change
add_column :users, :default_account_order, :string, default: "name_asc"
end
end

5
db/schema.rb generated
View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2025_07_24_115507) do
ActiveRecord::Schema[7.2].define(version: 2025_07_31_134449) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@@ -29,7 +29,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_07_24_115507) do
t.uuid "accountable_id"
t.decimal "balance", precision: 19, scale: 4
t.string "currency"
t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY ((ARRAY['Loan'::character varying, 'CreditCard'::character varying, 'OtherLiability'::character varying])::text[])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY (ARRAY[('Loan'::character varying)::text, ('CreditCard'::character varying)::text, ('OtherLiability'::character varying)::text])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
t.uuid "import_id"
t.uuid "plaid_account_id"
t.decimal "cash_balance", precision: 19, scale: 4, default: "0.0"
@@ -800,6 +800,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_07_24_115507) do
t.text "goals", default: [], array: true
t.datetime "set_onboarding_preferences_at"
t.datetime "set_onboarding_goals_at"
t.string "default_account_order", default: "name_asc"
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["family_id"], name: "index_users_on_family_id"
t.index ["last_viewed_chat_id"], name: "index_users_on_last_viewed_chat_id"