From 898770b913306b113fa7e93699be8e71937ca936 Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Thu, 13 Jun 2024 11:22:15 -0400 Subject: [PATCH 1/5] Add institution management --- app/controllers/accounts_controller.rb | 3 +- app/controllers/institutions_controller.rb | 35 ++++++++ app/helpers/institutions_helper.rb | 5 ++ .../profile_image_preview_controller.js | 2 +- app/models/account.rb | 5 +- app/models/family.rb | 1 + app/models/institution.rb | 5 ++ .../accounts/_accountable_group.html.erb | 17 ++++ app/views/accounts/_empty.html.erb | 11 +++ .../accounts/_institution_accounts.html.erb | 62 ++++++++++++++ .../_institutionless_accounts.html.erb | 17 ++++ app/views/accounts/index.html.erb | 78 +++++++----------- app/views/institutions/_form.html.erb | 27 ++++++ app/views/institutions/edit.html.erb | 10 +++ app/views/institutions/new.html.erb | 10 +++ app/views/settings/profiles/show.html.erb | 2 +- config/locales/views/accounts/en.yml | 17 ++++ config/locales/views/institutions/en.yml | 9 ++ config/routes.rb | 2 + .../20240612164751_create_institutions.rb | 11 +++ ...40612164944_add_institution_to_accounts.rb | 5 ++ db/schema.rb | 15 +++- test/controllers/accounts_controller_test.rb | 9 ++ .../institutions_controller_test.rb | 55 ++++++++++++ test/fixtures/accounts.yml | 5 ++ test/fixtures/active_storage/attachments.yml | 4 + test/fixtures/active_storage/blobs.yml | 1 + test/fixtures/files/square-placeholder.png | Bin 0 -> 7592 bytes test/fixtures/institutions.yml | 8 ++ 29 files changed, 379 insertions(+), 52 deletions(-) create mode 100644 app/controllers/institutions_controller.rb create mode 100644 app/helpers/institutions_helper.rb create mode 100644 app/models/institution.rb create mode 100644 app/views/accounts/_accountable_group.html.erb create mode 100644 app/views/accounts/_empty.html.erb create mode 100644 app/views/accounts/_institution_accounts.html.erb create mode 100644 app/views/accounts/_institutionless_accounts.html.erb create mode 100644 app/views/institutions/_form.html.erb create mode 100644 app/views/institutions/edit.html.erb create mode 100644 app/views/institutions/new.html.erb create mode 100644 config/locales/views/institutions/en.yml create mode 100644 db/migrate/20240612164751_create_institutions.rb create mode 100644 db/migrate/20240612164944_add_institution_to_accounts.rb create mode 100644 test/controllers/institutions_controller_test.rb create mode 100644 test/fixtures/active_storage/attachments.yml create mode 100644 test/fixtures/active_storage/blobs.yml create mode 100644 test/fixtures/files/square-placeholder.png create mode 100644 test/fixtures/institutions.yml diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index ad2cd8b7..52d5aeb3 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -5,7 +5,8 @@ class AccountsController < ApplicationController before_action :set_account, only: %i[ show destroy sync update ] def index - @accounts = Current.family.accounts + @institutions = Current.family.institutions + @accounts = Current.family.accounts.ungrouped.alphabetically end def summary diff --git a/app/controllers/institutions_controller.rb b/app/controllers/institutions_controller.rb new file mode 100644 index 00000000..482e437b --- /dev/null +++ b/app/controllers/institutions_controller.rb @@ -0,0 +1,35 @@ +class InstitutionsController < ApplicationController + before_action :set_institution, except: %i[ new create ] + + def new + @institution = Institution.new + end + + def create + Current.family.institutions.create!(institution_params) + redirect_to accounts_path, notice: "Institution created" + end + + def edit + end + + def update + @institution.update!(institution_params) + redirect_to accounts_path, notice: "Institution updated" + end + + def destroy + @institution.destroy! + redirect_to accounts_path, notice: "Institution deleted" + end + + private + + def institution_params + params.require(:institution).permit(:name, :logo) + end + + def set_institution + @institution = Current.family.institutions.find(params[:id]) + end +end diff --git a/app/helpers/institutions_helper.rb b/app/helpers/institutions_helper.rb new file mode 100644 index 00000000..3fa70e37 --- /dev/null +++ b/app/helpers/institutions_helper.rb @@ -0,0 +1,5 @@ +module InstitutionsHelper + def institution_logo(institution) + institution.logo.attached? ? institution.logo : institution.logo_url + end +end diff --git a/app/javascript/controllers/profile_image_preview_controller.js b/app/javascript/controllers/profile_image_preview_controller.js index 6118634b..50896625 100644 --- a/app/javascript/controllers/profile_image_preview_controller.js +++ b/app/javascript/controllers/profile_image_preview_controller.js @@ -8,7 +8,7 @@ export default class extends Controller { if (file) { const reader = new FileReader(); reader.onload = (e) => { - this.imagePreviewTarget.innerHTML = `Preview`; + this.imagePreviewTarget.innerHTML = `Preview`; this.templateTarget.classList.add("hidden"); this.clearBtnTarget.classList.remove("hidden"); }; diff --git a/app/models/account.rb b/app/models/account.rb index c77f498f..8869b62b 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -2,10 +2,12 @@ class Account < ApplicationRecord include Syncable include Monetizable + broadcasts_refreshes + validates :family, presence: true - broadcasts_refreshes belongs_to :family + belongs_to :institution, optional: true has_many :balances, dependent: :destroy has_many :valuations, dependent: :destroy has_many :transactions, dependent: :destroy @@ -19,6 +21,7 @@ class Account < ApplicationRecord scope :assets, -> { where(classification: "asset") } scope :liabilities, -> { where(classification: "liability") } scope :alphabetically, -> { order(:name) } + scope :ungrouped, -> { where(institution_id: nil) } delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy diff --git a/app/models/family.rb b/app/models/family.rb index 88c9b022..3b644bac 100644 --- a/app/models/family.rb +++ b/app/models/family.rb @@ -2,6 +2,7 @@ class Family < ApplicationRecord has_many :users, dependent: :destroy has_many :tags, dependent: :destroy has_many :accounts, dependent: :destroy + has_many :institutions, dependent: :destroy has_many :transactions, through: :accounts has_many :imports, through: :accounts has_many :transaction_categories, dependent: :destroy, class_name: "Transaction::Category" diff --git a/app/models/institution.rb b/app/models/institution.rb new file mode 100644 index 00000000..b30ba4fc --- /dev/null +++ b/app/models/institution.rb @@ -0,0 +1,5 @@ +class Institution < ApplicationRecord + belongs_to :family + has_many :accounts, dependent: :nullify + has_one_attached :logo +end diff --git a/app/views/accounts/_accountable_group.html.erb b/app/views/accounts/_accountable_group.html.erb new file mode 100644 index 00000000..4f052f5c --- /dev/null +++ b/app/views/accounts/_accountable_group.html.erb @@ -0,0 +1,17 @@ +<%# locals: (accounts:) %> + +<% accounts.group_by(&:accountable_type).each do |group, accounts| %> +
+
+

<%= to_accountable_title(Accountable.from_type(group)) %>

+ · +

<%= accounts.count %>

+

<%= format_money accounts.sum(&:balance_money) %>

+
+
+ <% accounts.each do |account| %> + <%= render account %> + <% end %> +
+
+<% end %> \ No newline at end of file diff --git a/app/views/accounts/_empty.html.erb b/app/views/accounts/_empty.html.erb new file mode 100644 index 00000000..42ec2c69 --- /dev/null +++ b/app/views/accounts/_empty.html.erb @@ -0,0 +1,11 @@ +
+
+ <%= tag.p t(".no_accounts"), class: "text-gray-900 mb-1 font-medium" %> + <%= tag.p t(".empty_message"), class: "text-gray-500 mb-4" %> + + <%= link_to new_account_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2 pr-3", data: { turbo_frame: "modal" } do %> + <%= lucide_icon("plus", class: "w-5 h-5") %> + <%= t(".new_account") %> + <% end %> +
+
\ No newline at end of file diff --git a/app/views/accounts/_institution_accounts.html.erb b/app/views/accounts/_institution_accounts.html.erb new file mode 100644 index 00000000..a2b51be9 --- /dev/null +++ b/app/views/accounts/_institution_accounts.html.erb @@ -0,0 +1,62 @@ +<%# locals: (institution:) %> + +
+ + <%= lucide_icon "chevron-right", class: "group-open:transform group-open:rotate-90 text-gray-500 w-5" %> + +
+ <% if institution_logo(institution) %> + <%= image_tag institution_logo(institution), class: "rounded-full h-full w-full" %> + <% else %> +
+ <%= tag.p institution.name.first.upcase, class: "text-blue-600 text-xs font-medium" %> +
+ <% end %> +
+ + <%= link_to institution.name, edit_institution_path(institution), data: { turbo_frame: :modal }, class: "text-sm font-medium text-gray-900 ml-1 mr-auto hover:underline" %> + + <%= contextual_menu do %> +
+ <%= link_to edit_institution_path(institution), + class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg", + data: { turbo_frame: :modal } do %> + <%= lucide_icon "pencil-line", class: "w-5 h-5 text-gray-500" %> + + <%= t(".edit") %> + <% end %> + + <%= button_to institution_path(institution), + method: :delete, + class: "block w-full py-2 px-3 space-x-2 text-red-600 hover:bg-red-50 flex items-center rounded-lg", + data: { + turbo_confirm: { + title: t(".confirm_title"), + body: t(".confirm_body"), + accept: t(".confirm_accept") + } + } do %> + <%= lucide_icon "trash-2", class: "w-5 h-5" %> + + <%= t(".delete") %> + <% end %> +
+ + + <% end %> +
+ +
+ <% if institution.accounts.any? %> + <%= render "accountable_group", accounts: institution.accounts %> + <% else %> +
+

There are no accounts in this financial institution

+ <%= link_to new_account_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-1.5 pr-2", data: { turbo_frame: "modal" } do %> + <%= lucide_icon("plus", class: "w-4 h-4") %> + <%= t(".new_account") %> + <% end %> +
+ <% end %> +
+
\ No newline at end of file diff --git a/app/views/accounts/_institutionless_accounts.html.erb b/app/views/accounts/_institutionless_accounts.html.erb new file mode 100644 index 00000000..1e0b41ba --- /dev/null +++ b/app/views/accounts/_institutionless_accounts.html.erb @@ -0,0 +1,17 @@ +<%# locals: (accounts:) %> + +
+ + <%= lucide_icon "chevron-right", class: "group-open:transform group-open:rotate-90 text-gray-500 w-5" %> + +
+ <%= lucide_icon("folder-pen", class: "w-5 h-5 text-gray-500") %> +
+ + <%= t(".other_accounts") %> +
+ +
+ <%= render "accountable_group", accounts: accounts %> +
+
\ No newline at end of file diff --git a/app/views/accounts/index.html.erb b/app/views/accounts/index.html.erb index 914dcfe7..6efc7b4c 100644 --- a/app/views/accounts/index.html.erb +++ b/app/views/accounts/index.html.erb @@ -1,62 +1,46 @@ <% content_for :sidebar do %> <%= render "settings/nav" %> <% end %> +
-
-

Accounts

- <%= link_to new_account_path, class: "flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2 pr-3", data: { turbo_frame: "modal" } do %> - <%= lucide_icon("plus", class: "w-5 h-5") %> - <%= t(".new_account") %> - <% end %> -
- <% if @accounts.empty? %> -
-
-

No accounts yet

-

Add an account either via connection, importing or entering manually.

- <%= link_to new_account_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2 pr-3", data: { turbo_frame: "modal" } do %> +
+

<%= t(".accounts") %>

+
+
+ <%= contextual_menu do %> +
+ <%= link_to new_institution_path, + class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg font-normal", + data: { turbo_frame: "modal" } do %> + <%= lucide_icon "building-2", class: "w-5 h-5 text-gray-500" %> + <%= t(".add_institution") %> + <% end %> +
+ <% end %> + + <%= link_to new_account_path, + data: { turbo_frame: "modal" }, + class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2" do %> <%= lucide_icon("plus", class: "w-5 h-5") %> - <%= t(".new_account") %> +

<%= t(".new_account") %>

<% end %>
+
+ + + <% if @accounts.empty? && @institutions.empty? %> + <%= render "empty" %> <% else %> -
- <% @accounts.by_provider.each do |item| %> -
- - <%= lucide_icon("chevron-down", class: "hidden group-open:block w-5 h-5 text-gray-500") %> - <%= lucide_icon("chevron-right", class: "group-open:hidden w-5 h-5 text-gray-500") %> - <% if item[:name] == "Manual accounts" %> -
- <%= lucide_icon("folder-pen", class: "w-5 h-5 text-gray-500") %> -
- <% end %> - - <%= item[:name] %> - -
-
- <% item[:accounts].each do |group, accounts| %> -
-
-

<%= to_accountable_title(Accountable.from_type(group)) %>

- · -

<%= accounts.count %>

-

<%= format_money accounts.sum(&:balance_money) %>

-
-
- <% accounts.each do |account| %> - <%= render account %> - <% end %> -
-
- <% end %> -
-
+
+ <% @institutions.each do |institution| %> + <%= render "institution_accounts", institution: %> <% end %> + + <%= render "institutionless_accounts", accounts: @accounts %>
<% end %> +
<% if self_hosted? %> <%= previous_setting("Self-Hosting", settings_hosting_path) %> diff --git a/app/views/institutions/_form.html.erb b/app/views/institutions/_form.html.erb new file mode 100644 index 00000000..272af071 --- /dev/null +++ b/app/views/institutions/_form.html.erb @@ -0,0 +1,27 @@ +<%= form_with model: institution, data: { turbo_frame: "_top", controller: "profile-image-preview" } do |f| %> + +
+ <%= f.label :logo do %> +
+ <% persisted_logo = institution_logo(institution) %> + + <% if persisted_logo %> + <%= image_tag persisted_logo, class: "absolute inset-0 rounded-full w-full h-full object-cover" %> + <% end %> + +
+ <% unless persisted_logo %> + <%= lucide_icon "image-plus", class: "w-5 h-5 text-gray-500 cursor-pointer", data: { profile_image_preview_target: "template" } %> + <% end %> +
+
+ <% end %> +
+ + <%= f.file_field :logo, + accept: "image/png, image/jpeg", + class: "hidden", + data: { profile_image_preview_target: "fileField", action: "profile-image-preview#preview" } %> + <%= f.text_field :name, label: t(".name") %> + <%= f.submit %> +<% end %> \ No newline at end of file diff --git a/app/views/institutions/edit.html.erb b/app/views/institutions/edit.html.erb new file mode 100644 index 00000000..83d2d9a5 --- /dev/null +++ b/app/views/institutions/edit.html.erb @@ -0,0 +1,10 @@ +<%= modal do %> +
+
+

<%= t(".edit", institution: @institution.name) %>

+ <%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %> +
+ + <%= render "form", institution: @institution %> +
+<% end %> \ No newline at end of file diff --git a/app/views/institutions/new.html.erb b/app/views/institutions/new.html.erb new file mode 100644 index 00000000..de698938 --- /dev/null +++ b/app/views/institutions/new.html.erb @@ -0,0 +1,10 @@ +<%= modal do %> +
+
+

<%= t(".new_institution") %>

+ <%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %> +
+ + <%= render "form", institution: @institution %> +
+<% end %> \ No newline at end of file diff --git a/app/views/settings/profiles/show.html.erb b/app/views/settings/profiles/show.html.erb index ea7cde13..64e0959f 100644 --- a/app/views/settings/profiles/show.html.erb +++ b/app/views/settings/profiles/show.html.erb @@ -8,7 +8,7 @@ <%= form_with model: Current.user, url: settings_profile_path, html: {data: { controller: "profile-image-preview" }} do |form| %>
-
+
<% profile_image_attached = Current.user.profile_image.attached? %> <% if profile_image_attached %>
diff --git a/config/locales/views/accounts/en.yml b/config/locales/views/accounts/en.yml index 9728b7fc..1f3aa5b6 100644 --- a/config/locales/views/accounts/en.yml +++ b/config/locales/views/accounts/en.yml @@ -12,12 +12,29 @@ en: success: New account created successfully destroy: success: Account deleted successfully + empty: + empty_message: Add an account either via connection, importing or entering manually. + new_account: New account + no_accounts: No accounts yet header: accounts: Accounts manage: Manage accounts new: New account index: + accounts: Accounts + add_institution: Add institution new_account: New account + institution_accounts: + confirm_accept: Delete institution + confirm_body: Don't worry, none of the accounts within this institution will + be affected by this deletion. Accounts will be ungrouped and all historical + data will remain intact. + confirm_title: Delete financial institution? + delete: Delete institution + edit: Edit institution + new_account: Add account + institutionless_accounts: + other_accounts: Other accounts new: balance: label: Balance diff --git a/config/locales/views/institutions/en.yml b/config/locales/views/institutions/en.yml new file mode 100644 index 00000000..ba106aa9 --- /dev/null +++ b/config/locales/views/institutions/en.yml @@ -0,0 +1,9 @@ +--- +en: + institutions: + edit: + edit: Edit %{institution} + form: + name: Financial institution name + new: + new_institution: New financial institution diff --git a/config/routes.rb b/config/routes.rb index 08c39db7..feb262e6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -71,6 +71,8 @@ Rails.application.routes.draw do resources :valuations end + resources :institutions, except: %i[ index show ] + # For managing self-hosted upgrades and release notifications resources :upgrades, only: [] do member do diff --git a/db/migrate/20240612164751_create_institutions.rb b/db/migrate/20240612164751_create_institutions.rb new file mode 100644 index 00000000..cb770022 --- /dev/null +++ b/db/migrate/20240612164751_create_institutions.rb @@ -0,0 +1,11 @@ +class CreateInstitutions < ActiveRecord::Migration[7.2] + def change + create_table :institutions, id: :uuid do |t| + t.string :name, null: false + t.string :logo_url + t.references :family, null: false, foreign_key: true, type: :uuid + + t.timestamps + end + end +end diff --git a/db/migrate/20240612164944_add_institution_to_accounts.rb b/db/migrate/20240612164944_add_institution_to_accounts.rb new file mode 100644 index 00000000..6050cb48 --- /dev/null +++ b/db/migrate/20240612164944_add_institution_to_accounts.rb @@ -0,0 +1,5 @@ +class AddInstitutionToAccounts < ActiveRecord::Migration[7.2] + def change + add_reference :accounts, :institution, foreign_key: true, type: :uuid + end +end diff --git a/db/schema.rb b/db/schema.rb index 08a9712e..56a0d4c0 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: 2024_05_24_203959) do +ActiveRecord::Schema[7.2].define(version: 2024_06_12_164944) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -93,8 +93,10 @@ ActiveRecord::Schema[7.2].define(version: 2024_05_24_203959) do t.jsonb "sync_warnings", default: [], null: false t.jsonb "sync_errors", default: [], null: false t.date "last_sync_date" + t.uuid "institution_id" t.index ["accountable_type"], name: "index_accounts_on_accountable_type" t.index ["family_id"], name: "index_accounts_on_family_id" + t.index ["institution_id"], name: "index_accounts_on_institution_id" end create_table "active_storage_attachments", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -234,6 +236,15 @@ ActiveRecord::Schema[7.2].define(version: 2024_05_24_203959) do t.index ["account_id"], name: "index_imports_on_account_id" end + create_table "institutions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string "name", null: false + t.string "logo_url" + t.uuid "family_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["family_id"], name: "index_institutions_on_family_id" + end + create_table "invite_codes", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.string "token", null: false t.datetime "created_at", null: false @@ -334,9 +345,11 @@ ActiveRecord::Schema[7.2].define(version: 2024_05_24_203959) do add_foreign_key "account_balances", "accounts", on_delete: :cascade add_foreign_key "accounts", "families" + add_foreign_key "accounts", "institutions" add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "imports", "accounts" + add_foreign_key "institutions", "families" add_foreign_key "taggings", "tags" add_foreign_key "tags", "families" add_foreign_key "transaction_categories", "families" diff --git a/test/controllers/accounts_controller_test.rb b/test/controllers/accounts_controller_test.rb index 9173e904..c4e155a4 100644 --- a/test/controllers/accounts_controller_test.rb +++ b/test/controllers/accounts_controller_test.rb @@ -6,6 +6,15 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest @account = accounts(:checking) end + test "gets accounts list" do + get accounts_url + assert_response :success + + @user.family.accounts.each do |account| + assert_dom "#" + dom_id(account), count: 1 + end + end + test "new" do get new_account_path assert_response :ok diff --git a/test/controllers/institutions_controller_test.rb b/test/controllers/institutions_controller_test.rb new file mode 100644 index 00000000..2b285bc1 --- /dev/null +++ b/test/controllers/institutions_controller_test.rb @@ -0,0 +1,55 @@ +require "test_helper" + +class InstitutionsControllerTest < ActionDispatch::IntegrationTest + setup do + sign_in users(:family_admin) + @institution = institutions(:chase) + end + + test "should get new" do + get new_institution_url + assert_response :success + end + + test "can create institution" do + assert_difference("Institution.count", 1) do + post institutions_url, params: { + institution: { + name: "New institution" + } + } + end + + assert_redirected_to accounts_url + assert_equal "Institution created", flash[:notice] + end + + test "should get edit" do + get edit_institution_url(@institution) + + assert_response :success + end + + test "should update institution" do + patch institution_url(@institution), params: { + institution: { + name: "New Institution Name", + logo: file_fixture_upload("square-placeholder.png", "image/png", :binary) + } + } + + assert_redirected_to accounts_url + assert_equal "Institution updated", flash[:notice] + end + + test "can destroy institution without destroying accounts" do + assert @institution.accounts.count > 0 + + assert_difference -> { Institution.count } => -1, -> { Account.count } => 0 do + delete institution_url(@institution) + end + + assert_redirected_to accounts_url + assert_equal "Institution deleted", flash[:notice] + end +end diff --git a/test/fixtures/accounts.yml b/test/fixtures/accounts.yml index cc95eaee..1859f8d5 100644 --- a/test/fixtures/accounts.yml +++ b/test/fixtures/accounts.yml @@ -13,6 +13,7 @@ checking: balance: 5000 accountable_type: Account::Depository accountable_id: "123e4567-e89b-12d3-a456-426614174000" + institution: chase # Account with both transactions and valuations savings_with_valuation_overrides: @@ -21,6 +22,7 @@ savings_with_valuation_overrides: balance: 20000 accountable_type: Account::Depository accountable_id: "123e4567-e89b-12d3-a456-426614174001" + institution: chase # Liability account credit_card: @@ -29,6 +31,7 @@ credit_card: balance: 1000 accountable_type: Account::Credit accountable_id: "123e4567-e89b-12d3-a456-426614174003" + institution: chase eur_checking: family: dylan_family @@ -37,6 +40,7 @@ eur_checking: balance: 12000 accountable_type: Account::Depository accountable_id: "123e4567-e89b-12d3-a456-426614174004" + institution: revolut # Multi-currency account (e.g. Wise, Revolut, etc.) multi_currency: @@ -46,3 +50,4 @@ multi_currency: balance: 10000 accountable_type: Account::Depository accountable_id: "123e4567-e89b-12d3-a456-426614174005" + institution: revolut diff --git a/test/fixtures/active_storage/attachments.yml b/test/fixtures/active_storage/attachments.yml new file mode 100644 index 00000000..131a9e01 --- /dev/null +++ b/test/fixtures/active_storage/attachments.yml @@ -0,0 +1,4 @@ +chase_logo_attachment: + name: logo + record: chase (Institution) + blob: square_placeholder_blob diff --git a/test/fixtures/active_storage/blobs.yml b/test/fixtures/active_storage/blobs.yml new file mode 100644 index 00000000..70bcd180 --- /dev/null +++ b/test/fixtures/active_storage/blobs.yml @@ -0,0 +1 @@ +square_placeholder_blob: <%= ActiveStorage::FixtureSet.blob filename: "square-placeholder.png" %> \ No newline at end of file diff --git a/test/fixtures/files/square-placeholder.png b/test/fixtures/files/square-placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..32bd85e9de2f178367ff99f409ffc830dbcfc33c GIT binary patch literal 7592 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 g(f@DW`m5{Sl}npS8aNu$=+!Ljr~N? -- 2.53.0 From 5424301c5ad3eaa8f826d16c7c9ef275dca8ca99 Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Thu, 13 Jun 2024 12:39:24 -0400 Subject: [PATCH 2/5] Allow user to select institution on create or edit --- app/controllers/accounts_controller.rb | 10 ++++++--- app/models/institution.rb | 2 ++ app/views/accounts/_account.html.erb | 6 +++++- app/views/accounts/_account_type.html.erb | 2 +- .../accounts/_accountable_group.html.erb | 2 +- app/views/accounts/_empty.html.erb | 2 +- app/views/accounts/_entry_method.html.erb | 2 +- .../accounts/_institution_accounts.html.erb | 5 ++--- .../_institutionless_accounts.html.erb | 2 +- app/views/accounts/edit.html.erb | 21 +++++++++++++++++++ app/views/accounts/index.html.erb | 1 - app/views/accounts/new.html.erb | 3 ++- app/views/accounts/show.html.erb | 14 +++++++------ app/views/institutions/_form.html.erb | 2 +- app/views/institutions/edit.html.erb | 2 +- app/views/institutions/new.html.erb | 2 +- app/views/shared/_modal.html.erb | 2 +- config/locales/views/accounts/en.yml | 7 +++++++ test/controllers/accounts_controller_test.rb | 8 +++++-- 19 files changed, 69 insertions(+), 26 deletions(-) create mode 100644 app/views/accounts/edit.html.erb diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 3ff5a890..f037890f 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -2,7 +2,7 @@ class AccountsController < ApplicationController layout "with_sidebar" include Filterable - before_action :set_account, only: %i[ show destroy sync update ] + before_action :set_account, only: %i[ edit show destroy sync update ] after_action :sync_account, only: :create def index @@ -25,7 +25,8 @@ class AccountsController < ApplicationController def new @account = Account.new( balance: nil, - accountable: Accountable.from_type(params[:type])&.new + accountable: Accountable.from_type(params[:type])&.new, + institution_id: params[:institution_id], ) end @@ -34,6 +35,9 @@ class AccountsController < ApplicationController @valuation_series = @account.valuations.to_series end + def edit + end + def update @account.update! account_params.except(:accountable_type) redirect_back_or_to account_path(@account), notice: t(".success") @@ -81,7 +85,7 @@ class AccountsController < ApplicationController end def account_params - params.require(:account).permit(:name, :accountable_type, :balance, :start_date, :start_balance, :currency, :subtype, :is_active) + params.require(:account).permit(:name, :accountable_type, :balance, :start_date, :start_balance, :currency, :subtype, :is_active, :institution_id) end def sync_account diff --git a/app/models/institution.rb b/app/models/institution.rb index b30ba4fc..3d9f13db 100644 --- a/app/models/institution.rb +++ b/app/models/institution.rb @@ -2,4 +2,6 @@ class Institution < ApplicationRecord belongs_to :family has_many :accounts, dependent: :nullify has_one_attached :logo + + scope :alphabetically, -> { order(name: :asc) } end diff --git a/app/views/accounts/_account.html.erb b/app/views/accounts/_account.html.erb index ed3fc6fd..715570c7 100644 --- a/app/views/accounts/_account.html.erb +++ b/app/views/accounts/_account.html.erb @@ -1,10 +1,14 @@ <%= turbo_frame_tag dom_id(account) do %> -
+
"> <%= account.name[0].upcase %>
<%= link_to account.name, account, class: [(account.is_active ? "text-gray-900" : "text-gray-400"), "text-sm font-medium hover:underline"], data: { turbo_frame: "_top" } %> + + <%= link_to edit_account_path(account), data: { turbo_frame: :modal }, class: "group-hover/account:flex hidden hover:opacity-80 items-center justify-center" do %> + <%= lucide_icon "pencil-line", class: "w-4 h-4 text-gray-500" %> + <% end %>

"> diff --git a/app/views/accounts/_account_type.html.erb b/app/views/accounts/_account_type.html.erb index b37ebe29..be85f847 100644 --- a/app/views/accounts/_account_type.html.erb +++ b/app/views/accounts/_account_type.html.erb @@ -1,4 +1,4 @@ -<%= link_to new_account_path(step: "method", type: type.class.name.demodulize), class: "flex items-center gap-4 w-full text-center focus:outline-none focus:bg-gray-25 border border-transparent focus:border focus:border-gray-200 block px-2 hover:bg-gray-25 rounded-lg p-2" do %> +<%= link_to new_account_path(step: "method", type: type.class.name.demodulize, institution_id: params[:institution_id]), class: "flex items-center gap-4 w-full text-center focus:outline-none focus:bg-gray-25 border border-transparent focus:border focus:border-gray-200 block px-2 hover:bg-gray-25 rounded-lg p-2" do %> <%= lucide_icon(icon, class: "#{text_color} w-5 h-5") %> diff --git a/app/views/accounts/_accountable_group.html.erb b/app/views/accounts/_accountable_group.html.erb index 4f052f5c..199c8fe1 100644 --- a/app/views/accounts/_accountable_group.html.erb +++ b/app/views/accounts/_accountable_group.html.erb @@ -14,4 +14,4 @@ <% end %>

-<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/accounts/_empty.html.erb b/app/views/accounts/_empty.html.erb index 42ec2c69..e938c863 100644 --- a/app/views/accounts/_empty.html.erb +++ b/app/views/accounts/_empty.html.erb @@ -8,4 +8,4 @@ <%= t(".new_account") %> <% end %>
-
\ No newline at end of file +
diff --git a/app/views/accounts/_entry_method.html.erb b/app/views/accounts/_entry_method.html.erb index ad13d55c..ea73af73 100644 --- a/app/views/accounts/_entry_method.html.erb +++ b/app/views/accounts/_entry_method.html.erb @@ -6,7 +6,7 @@ <%= text %> <% else %> - <%= link_to new_account_path(type: type.class.name.demodulize), class: "flex items-center gap-4 w-full text-center focus:outline-none focus:bg-gray-50 border border-transparent focus:border focus:border-gray-200 px-2 hover:bg-gray-50 rounded-lg p-2" do %> + <%= link_to new_account_path(type: type.class.name.demodulize, institution_id: params[:institution_id]), class: "flex items-center gap-4 w-full text-center focus:outline-none focus:bg-gray-50 border border-transparent focus:border focus:border-gray-200 px-2 hover:bg-gray-50 rounded-lg p-2" do %> <%= lucide_icon(icon, class: "text-gray-500 w-5 h-5") %> diff --git a/app/views/accounts/_institution_accounts.html.erb b/app/views/accounts/_institution_accounts.html.erb index a2b51be9..d1d738ac 100644 --- a/app/views/accounts/_institution_accounts.html.erb +++ b/app/views/accounts/_institution_accounts.html.erb @@ -42,7 +42,6 @@ <% end %>
- <% end %> @@ -52,11 +51,11 @@ <% else %>

There are no accounts in this financial institution

- <%= link_to new_account_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-1.5 pr-2", data: { turbo_frame: "modal" } do %> + <%= link_to new_account_path(institution_id: institution.id), class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-1.5 pr-2", data: { turbo_frame: "modal" } do %> <%= lucide_icon("plus", class: "w-4 h-4") %> <%= t(".new_account") %> <% end %>
<% end %>
- \ No newline at end of file + diff --git a/app/views/accounts/_institutionless_accounts.html.erb b/app/views/accounts/_institutionless_accounts.html.erb index 1e0b41ba..335e842e 100644 --- a/app/views/accounts/_institutionless_accounts.html.erb +++ b/app/views/accounts/_institutionless_accounts.html.erb @@ -14,4 +14,4 @@
<%= render "accountable_group", accounts: accounts %>
- \ No newline at end of file + diff --git a/app/views/accounts/edit.html.erb b/app/views/accounts/edit.html.erb new file mode 100644 index 00000000..9f593f74 --- /dev/null +++ b/app/views/accounts/edit.html.erb @@ -0,0 +1,21 @@ +<%= modal do %> +
+
+

<%= t(".edit", account: @account.name) %>

+ <%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %> +
+ + <%= form_with model: @account, data: { turbo_frame: "_top" } do |f| %> + <%= f.text_field :name, label: "Name" %> + +
+ <%= f.collection_select :institution_id, Current.family.institutions.alphabetically, :id, :name, { include_blank: t(".ungrouped"), label: t(".institution") } %> + <%= link_to new_institution_path do %> + <%= lucide_icon "plus", class: "text-gray-700 hover:text-gray-500 w-4 h-4 absolute right-3 top-2" %> + <% end %> +
+ + <%= f.submit %> + <% end %> +
+<% end %> diff --git a/app/views/accounts/index.html.erb b/app/views/accounts/index.html.erb index 6efc7b4c..9c5a88b0 100644 --- a/app/views/accounts/index.html.erb +++ b/app/views/accounts/index.html.erb @@ -28,7 +28,6 @@
- <% if @accounts.empty? && @institutions.empty? %> <%= render "empty" %> <% else %> diff --git a/app/views/accounts/new.html.erb b/app/views/accounts/new.html.erb index c0518c00..4bfd6c21 100644 --- a/app/views/accounts/new.html.erb +++ b/app/views/accounts/new.html.erb @@ -76,6 +76,7 @@
<%= f.hidden_field :accountable_type %> <%= f.text_field :name, placeholder: t(".name.placeholder"), required: "required", label: t(".name.label"), autofocus: true %> + <%= f.collection_select :institution_id, Current.family.institutions.alphabetically, :id, :name, { include_blank: t(".ungrouped"), label: t(".institution") } %> <%= render "accounts/#{permitted_accountable_partial(@account.accountable_type)}", f: f %> <%= f.money_field :balance_money, label: t(".balance"), required: "required" %> @@ -83,7 +84,7 @@ <%= check_box_tag :add_start_values, class: "maybe-checkbox maybe-checkbox--light peer mb-1" %> <%= label_tag :add_start_values, t(".optional_start_balance_message"), class: "pl-1 text-sm text-gray-500" %> -