From 6d4a5dd743fd4e6e40d48d339b81aed2bfe000c5 Mon Sep 17 00:00:00 2001 From: Aluisio Pereira Date: Mon, 11 Aug 2025 17:58:44 -0300 Subject: [PATCH] Add customizable menu order for user accounts (#44) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-authored-by: Juan José Mata --- app/controllers/concerns/orderable.rb | 14 +++++ app/controllers/users_controller.rb | 2 +- app/models/account_order.rb | 60 +++++++++++++++++++ app/models/balance_sheet.rb | 22 ++++++- app/models/user.rb | 5 ++ app/views/settings/preferences/show.html.erb | 5 ++ config/locales/views/settings/en.yml | 1 + ...4449_add_default_account_order_to_users.rb | 5 ++ db/schema.rb | 5 +- 9 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 app/controllers/concerns/orderable.rb create mode 100644 app/models/account_order.rb create mode 100644 db/migrate/20250731134449_add_default_account_order_to_users.rb diff --git a/app/controllers/concerns/orderable.rb b/app/controllers/concerns/orderable.rb new file mode 100644 index 000000000..2bc94964f --- /dev/null +++ b/app/controllers/concerns/orderable.rb @@ -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 diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2b3c0866a..6632a47fa 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -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: [] ) diff --git a/app/models/account_order.rb b/app/models/account_order.rb new file mode 100644 index 000000000..8a26c0fc4 --- /dev/null +++ b/app/models/account_order.rb @@ -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 diff --git a/app/models/balance_sheet.rb b/app/models/balance_sheet.rb index f7b8cfd8a..e8f5f8442 100644 --- a/app/models/balance_sheet.rb +++ b/app/models/balance_sheet.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 501e95b87..901a5eca6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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? diff --git a/app/views/settings/preferences/show.html.erb b/app/views/settings/preferences/show.html.erb index fae15f30e..fd108a725 100644 --- a/app/views/settings/preferences/show.html.erb +++ b/app/views/settings/preferences/show.html.erb @@ -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") }, diff --git a/config/locales/views/settings/en.yml b/config/locales/views/settings/en.yml index 6837b4b1b..f13a16ad1 100644 --- a/config/locales/views/settings/en.yml +++ b/config/locales/views/settings/en.yml @@ -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 diff --git a/db/migrate/20250731134449_add_default_account_order_to_users.rb b/db/migrate/20250731134449_add_default_account_order_to_users.rb new file mode 100644 index 000000000..8f0fcf712 --- /dev/null +++ b/db/migrate/20250731134449_add_default_account_order_to_users.rb @@ -0,0 +1,5 @@ +class AddDefaultAccountOrderToUsers < ActiveRecord::Migration[7.2] + def change + add_column :users, :default_account_order, :string, default: "name_asc" + end +end diff --git a/db/schema.rb b/db/schema.rb index 5984a8f2a..8d4bb0d8a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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"