<%= render "accounts/logo", account: account, size: "md" %>
@@ -16,41 +18,26 @@
<% else %>
-
+
<%= link_to account.name, account, class: [(account.active? ? "text-primary" : "text-subdued"), "text-sm font-medium hover:underline"], data: { turbo_frame: "_top" } %>
- <% if account.institution_name %>
- • <%= account.institution_name %>
+
+ <% if account.institution_name.present? %>
+ • <%= account.institution_name %>
<% end %>
<% if account.long_subtype_label %>
<%= account.long_subtype_label %>
<% end %>
+ <% if account.supports_default? && is_default %>
+
<%= t("accounts.account.default_label") %>
+ <% end %>
+ <% if account.institution_name.present? %>
+
<%= account.institution_name %>
+ <% end %>
<% end %>
-
- <% unless account.pending_deletion? %>
- <%= link_to edit_account_path(account, return_to: return_to), data: { turbo_frame: :modal }, class: "group-hover/account:flex hidden hover:opacity-80 items-center justify-center" do %>
- <%= icon("pencil-line", size: "sm") %>
- <% end %>
-
- <% if !account.linked? && ["Depository", "CreditCard", "Investment", "Crypto"].include?(account.accountable_type) %>
- <%= link_to select_provider_account_path(account),
- data: { turbo_frame: :modal },
- class: "group-hover/account:flex hidden hover:opacity-80 items-center justify-center gap-1",
- title: t("accounts.account.link_provider") do %>
- <%= icon("link", size: "sm") %>
- <% end %>
- <% elsif account.linked? %>
- <%= link_to confirm_unlink_account_path(account),
- data: { turbo_frame: :modal },
- class: "group-hover/account:flex hidden hover:opacity-80 items-center justify-center gap-1",
- title: t("accounts.account.unlink_provider") do %>
- <%= icon("unlink", size: "sm") %>
- <% end %>
- <% end %>
- <% end %>
+
<% if account.draft? %>
<% elsif account.syncing? %>
@@ -68,14 +55,34 @@
variant: :outline,
frame: :modal
) %>
- <% elsif account.active? || account.disabled? %>
- <%= form_with model: account, url: toggle_active_account_path(account), method: :patch, data: { turbo_frame: "_top", controller: "auto-submit-form" } do |f| %>
- <%= render DS::Toggle.new(
- id: "account_#{account.id}_active",
- name: "active",
- checked: account.active?,
- data: { auto_submit_form_target: "auto" }
- ) %>
+ <% elsif !account.pending_deletion? %>
+ <%= render DS::Menu.new(icon_vertical: true, mobile_fullwidth: false, max_width: "280px") do |menu| %>
+ <% menu.with_item(variant: "link", text: t("accounts.account.edit"), href: edit_account_path(account, return_to: return_to), icon: "pencil-line", data: { turbo_frame: :modal }) %>
+
+ <% if !account.linked? && %w[Depository CreditCard Investment Crypto].include?(account.accountable_type) %>
+ <% menu.with_item(variant: "link", text: t("accounts.account.link_provider"), href: select_provider_account_path(account), icon: "link", data: { turbo_frame: :modal }) %>
+ <% elsif account.linked? %>
+ <% menu.with_item(variant: "link", text: t("accounts.account.unlink_provider"), href: confirm_unlink_account_path(account), icon: "unlink", data: { turbo_frame: :modal }) %>
+ <% end %>
+
+ <% menu.with_item(variant: "divider") %>
+
+ <% if account.active? %>
+ <% menu.with_item(variant: "button", text: t("accounts.account.disable"), href: toggle_active_account_path(account), method: :patch, icon: "toggle-right", data: { turbo_frame: :_top }) %>
+ <% elsif account.disabled? %>
+ <% menu.with_item(variant: "button", text: t("accounts.account.enable"), href: toggle_active_account_path(account), method: :patch, icon: "toggle-left", data: { turbo_frame: :_top }) %>
+ <% end %>
+
+ <% if is_default %>
+ <% menu.with_item(variant: "button", text: t("accounts.account.remove_default"), href: remove_default_account_path(account), method: :patch, icon: "star-off", data: { turbo_frame: :_top }) %>
+ <% elsif account.eligible_for_transaction_default? %>
+ <% menu.with_item(variant: "button", text: t("accounts.account.set_default"), href: set_default_account_path(account), method: :patch, icon: "star", data: { turbo_frame: :_top }) %>
+ <% end %>
+
+ <% unless account.linked? %>
+ <% menu.with_item(variant: "divider") %>
+ <% menu.with_item(variant: "button", text: t("accounts.account.delete"), href: account_path(account), method: :delete, icon: "trash-2", confirm: CustomConfirm.for_resource_deletion("account", high_severity: true), data: { turbo_frame: :_top }) %>
+ <% end %>
<% end %>
<% end %>
diff --git a/app/views/transactions/_form.html.erb b/app/views/transactions/_form.html.erb
index 2717dd57c..7bfde6e9a 100644
--- a/app/views/transactions/_form.html.erb
+++ b/app/views/transactions/_form.html.erb
@@ -18,7 +18,7 @@
<% if @entry.account_id %>
<%= f.hidden_field :account_id %>
<% else %>
- <%= f.collection_select :account_id, Current.family.accounts.manual.active.alphabetically, :id, :name, { prompt: t(".account_prompt"), label: t(".account"), variant: :logo }, required: true, class: "form-field__input text-ellipsis" %>
+ <%= f.collection_select :account_id, Current.family.accounts.manual.active.alphabetically, :id, :name, { prompt: t(".account_prompt"), label: t(".account"), selected: Current.user.default_account_for_transactions&.id, variant: :logo }, required: true, class: "form-field__input text-ellipsis" %>
<% end %>
<%= f.money_field :amount, label: t(".amount"), required: true %>
diff --git a/config/locales/views/accounts/de.yml b/config/locales/views/accounts/de.yml
index 1b3e4d27b..f163a31b1 100644
--- a/config/locales/views/accounts/de.yml
+++ b/config/locales/views/accounts/de.yml
@@ -2,14 +2,23 @@
de:
accounts:
account:
+ edit: Bearbeiten
link_lunchflow: Mit Lunch Flow verknüpfen
link_provider: Mit Provider verknüpfen
unlink_provider: Von Provider trennen
troubleshoot: Fehlerbehebung
+ enable: Konto aktivieren
+ disable: Konto deaktivieren
+ set_default: Als Standard festlegen
+ remove_default: Standard aufheben
+ default_label: Standard
+ delete: Konto löschen
chart:
data_not_available: Für den ausgewählten Zeitraum sind keine Daten verfügbar
create:
success: "%{type}-Konto erstellt"
+ set_default:
+ depository_only: "Nur Bargeld- und Kreditkartenkonten können als Standard festgelegt werden."
destroy:
success: "%{type}-Konto zur Löschung vorgemerkt"
cannot_delete_linked: "Ein verknüpftes Konto kann nicht gelöscht werden. Bitte trennen Sie es zuerst."
diff --git a/config/locales/views/accounts/en.yml b/config/locales/views/accounts/en.yml
index 4e2d61faa..1a721e19a 100644
--- a/config/locales/views/accounts/en.yml
+++ b/config/locales/views/accounts/en.yml
@@ -2,14 +2,23 @@
en:
accounts:
account:
+ edit: Edit
link_lunchflow: Link with Lunch Flow
link_provider: Link with provider
unlink_provider: Unlink from provider
troubleshoot: Troubleshoot
+ enable: Enable account
+ disable: Disable account
+ set_default: Set as default
+ remove_default: Unset default
+ default_label: Default
+ delete: Delete account
chart:
data_not_available: Data not available for the selected period
create:
success: "%{type} account created"
+ set_default:
+ depository_only: "Only cash and credit card accounts can be set as default."
destroy:
success: "%{type} account scheduled for deletion"
cannot_delete_linked: "Cannot delete a linked account. Please unlink it first."
diff --git a/config/locales/views/accounts/es.yml b/config/locales/views/accounts/es.yml
index 2639652df..613bf93ce 100644
--- a/config/locales/views/accounts/es.yml
+++ b/config/locales/views/accounts/es.yml
@@ -2,14 +2,23 @@
es:
accounts:
account:
+ edit: Editar
link_lunchflow: Vincular con Lunch Flow
link_provider: Vincular con proveedor
unlink_provider: Desvincular de proveedor
troubleshoot: Solucionar problemas
+ enable: Activar cuenta
+ disable: Desactivar cuenta
+ set_default: Establecer como predeterminada
+ remove_default: Quitar predeterminada
+ default_label: Predeterminada
+ delete: Eliminar cuenta
chart:
data_not_available: Datos no disponibles para el período seleccionado
create:
success: "Cuenta %{type} creada"
+ set_default:
+ depository_only: "Solo las cuentas de efectivo y tarjeta de crédito pueden establecerse como predeterminadas."
destroy:
success: "Cuenta %{type} programada para eliminación"
cannot_delete_linked: "No se puede eliminar una cuenta vinculada. Por favor, desvincúlela primero."
diff --git a/config/locales/views/accounts/fr.yml b/config/locales/views/accounts/fr.yml
index 3c9dae3bb..7d2eafcb8 100644
--- a/config/locales/views/accounts/fr.yml
+++ b/config/locales/views/accounts/fr.yml
@@ -2,14 +2,23 @@
fr:
accounts:
account:
+ edit: Modifier
link_lunchflow: Lier avec Lunch Flow
link_provider: Lier avec un fournisseur
unlink_provider: Délier du fournisseur
troubleshoot: Dépannage
+ enable: Activer le compte
+ disable: Désactiver le compte
+ set_default: Définir par défaut
+ remove_default: Retirer par défaut
+ default_label: Par défaut
+ delete: Supprimer le compte
chart:
data_not_available: Données non disponibles pour la période sélectionnée
create:
success: "Compte %{type} créé"
+ set_default:
+ depository_only: "Seuls les comptes de liquidités et de carte de crédit peuvent être définis par défaut."
destroy:
success: "Le compte %{type} a été préparé à la suppression"
cannot_delete_linked: "Impossible de supprimer un compte lié. Veuillez d'abord le délier."
diff --git a/config/routes.rb b/config/routes.rb
index e3b494761..b4576b907 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -330,6 +330,8 @@ Rails.application.routes.draw do
post :sync
get :sparkline
patch :toggle_active
+ patch :set_default
+ patch :remove_default
get :select_provider
get :confirm_unlink
delete :unlink
diff --git a/db/migrate/20260305120000_add_default_account_to_users.rb b/db/migrate/20260305120000_add_default_account_to_users.rb
new file mode 100644
index 000000000..9bb6d0619
--- /dev/null
+++ b/db/migrate/20260305120000_add_default_account_to_users.rb
@@ -0,0 +1,5 @@
+class AddDefaultAccountToUsers < ActiveRecord::Migration[7.2]
+ def change
+ add_reference :users, :default_account, type: :uuid, foreign_key: { to_table: :accounts, on_delete: :nullify }, null: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ae425569d..08a91cddd 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1474,6 +1474,8 @@ ActiveRecord::Schema[7.2].define(version: 2026_03_14_131357) do
t.jsonb "preferences", default: {}, null: false
t.string "locale"
t.string "ui_layout"
+ t.uuid "default_account_id"
+ t.index ["default_account_id"], name: "index_users_on_default_account_id"
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"
@@ -1584,6 +1586,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_03_14_131357) do
add_foreign_key "transactions", "merchants"
add_foreign_key "transfers", "transactions", column: "inflow_transaction_id", on_delete: :cascade
add_foreign_key "transfers", "transactions", column: "outflow_transaction_id", on_delete: :cascade
+ add_foreign_key "users", "accounts", column: "default_account_id", on_delete: :nullify
add_foreign_key "users", "chats", column: "last_viewed_chat_id"
add_foreign_key "users", "families"
end
diff --git a/test/controllers/accounts_controller_test.rb b/test/controllers/accounts_controller_test.rb
index 39e4dc2e1..0034360e0 100644
--- a/test/controllers/accounts_controller_test.rb
+++ b/test/controllers/accounts_controller_test.rb
@@ -158,7 +158,6 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
assert_includes @response.body, @account.name
- assert_includes @response.body, "account_#{@account.id}_active"
end
test "toggle_active disables and re-enables an account" do
@@ -178,6 +177,34 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
end
+ test "set_default sets user default account" do
+ patch set_default_account_url(@account)
+ assert_redirected_to accounts_path
+ @user.reload
+ assert_equal @account.id, @user.default_account_id
+ end
+
+ test "set_default rejects ineligible account type" do
+ investment = accounts(:investment)
+
+ patch set_default_account_url(investment)
+ assert_redirected_to accounts_path
+ assert_equal I18n.t("accounts.set_default.depository_only"), flash[:alert]
+
+ @user.reload
+ assert_not_equal investment.id, @user.default_account_id
+ end
+
+ test "remove_default clears user default account" do
+ @user.update!(default_account: @account)
+
+ patch remove_default_account_url(@account)
+ assert_redirected_to accounts_path
+
+ @user.reload
+ assert_nil @user.default_account_id
+ end
+
test "select_provider redirects for already linked account" do
plaid_account = plaid_accounts(:one)
AccountProvider.create!(account: @account, provider: plaid_account)
diff --git a/test/models/user_test.rb b/test/models/user_test.rb
index 0d2a09d58..46aa0637f 100644
--- a/test/models/user_test.rb
+++ b/test/models/user_test.rb
@@ -362,6 +362,33 @@ class UserTest < ActiveSupport::TestCase
"Should return false when section key is missing from collapsed_sections"
end
+ # Default account for transactions
+ test "default_account_for_transactions returns account when active and manual" do
+ account = accounts(:depository)
+ @user.update!(default_account: account)
+ assert_equal account, @user.default_account_for_transactions
+ end
+
+ test "default_account_for_transactions returns nil when account is disabled" do
+ account = accounts(:depository)
+ @user.update!(default_account: account)
+ account.disable!
+ assert_nil @user.default_account_for_transactions
+ end
+
+ test "default_account_for_transactions returns nil when account is linked" do
+ account = accounts(:depository)
+ @user.update!(default_account: account)
+ plaid_account = plaid_accounts(:one)
+ AccountProvider.create!(account: account, provider: plaid_account)
+ account.reload
+ assert_nil @user.default_account_for_transactions
+ end
+
+ test "default_account_for_transactions returns nil when no default set" do
+ assert_nil @user.default_account_for_transactions
+ end
+
# SSO-only user security tests
test "sso_only? returns true for user with OIDC identity and no password" do
sso_user = users(:sso_only)