diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index 8529f781..d09dd635 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -49,6 +49,10 @@ .form-field__submit { @apply w-full p-3 text-center text-white bg-black rounded-lg hover:bg-gray-700; } + + input:checked + label + .toggle-switch-dot { + transform: translateX(100%); + } } /* Small, single purpose classes that should take precedence over other styles */ diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 7a726dc6..392c8bd5 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -15,6 +15,30 @@ class AccountsController < ApplicationController @valuation_series = @account.valuations.to_series(@account) end + def edit + end + + def update + @account = Current.family.accounts.find(params[:id]) + + if @account.update(account_params.except(:accountable_type)) + + @account.sync_later if account_params[:is_active] == "1" + + respond_to do |format| + format.html { redirect_to accounts_path, notice: t(".success") } + format.turbo_stream do + render turbo_stream: [ + turbo_stream.append("notification-tray", partial: "shared/notification", locals: { type: "success", content: t(".success") }), + turbo_stream.replace("account_#{@account.id}", partial: "accounts/account", locals: { account: @account }) + ] + end + end + else + render "edit", status: :unprocessable_entity + end + end + def create @account = Current.family.accounts.build(account_params.except(:accountable_type)) @account.accountable = Accountable.from_type(account_params[:accountable_type])&.new @@ -29,6 +53,6 @@ class AccountsController < ApplicationController private def account_params - params.require(:account).permit(:name, :accountable_type, :balance, :currency, :subtype) + params.require(:account).permit(:name, :accountable_type, :balance, :currency, :subtype, :is_active) end end diff --git a/app/models/account.rb b/app/models/account.rb index 3f4442a9..0a280f77 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -7,6 +7,8 @@ class Account < ApplicationRecord has_many :valuations has_many :transactions + scope :active, -> { where(is_active: true) } + delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy before_create :check_currency @@ -17,10 +19,15 @@ class Account < ApplicationRecord Trend.new(current: last.balance, previous: first.balance, type: classification) end + def self.by_provider + # TODO: When 3rd party providers are supported, dynamically load all providers and their accounts + [ { name: "Manual accounts", accounts: all.order(balance: :desc).group_by(&:accountable_type) } ] + end + # TODO: We will need a better way to encapsulate large queries & transformation logic, but leaving all in one spot until # we have a better understanding of the requirements def self.by_group(period = Period.all) - ranked_balances_cte = joins(:balances) + ranked_balances_cte = active.joins(:balances) .select(" account_balances.account_id, account_balances.balance, diff --git a/app/models/family.rb b/app/models/family.rb index 32261214..e514a783 100644 --- a/app/models/family.rb +++ b/app/models/family.rb @@ -4,15 +4,15 @@ class Family < ApplicationRecord has_many :transactions, through: :accounts def net_worth - accounts.sum("CASE WHEN classification = 'asset' THEN balance ELSE -balance END") + accounts.active.sum("CASE WHEN classification = 'asset' THEN balance ELSE -balance END") end def assets - accounts.where(classification: "asset").sum(:balance) + accounts.active.where(classification: "asset").sum(:balance) end def liabilities - accounts.where(classification: "liability").sum(:balance) + accounts.active.where(classification: "liability").sum(:balance) end def net_worth_series(period = nil) diff --git a/app/views/accounts/_account.html.erb b/app/views/accounts/_account.html.erb new file mode 100644 index 00000000..745c9549 --- /dev/null +++ b/app/views/accounts/_account.html.erb @@ -0,0 +1,23 @@ +<%= turbo_frame_tag dom_id(account) do %> +
+
+
"> + <%= account.name[0].upcase %> +
+

"> + <%= account.name %> +

+
+
+

"> + <%= format_currency account.balance %> +

+ <%= form_with model: account, method: :patch, html: { class: "flex items-center", data: { turbo_frame: "_top" } } do |form| %> +
+ <%= form.check_box :is_active, class: "sr-only peer", id: "is_active_#{account.id}", onchange: "this.form.requestSubmit();" %> + +
+ <% end %> +
+
+<% end%> \ No newline at end of file diff --git a/app/views/accounts/index.html.erb b/app/views/accounts/index.html.erb index 31590101..f1e54e7a 100644 --- a/app/views/accounts/index.html.erb +++ b/app/views/accounts/index.html.erb @@ -1,17 +1,57 @@ - -

<%= t('.title')%>

-

<%#= number_to_currency Current.family.cash_balance %>

- -<% Current.family.accounts.each do |account| %> -
-
- <%= account.name %> +
+
+

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", data: { turbo_frame: "modal" } do %> + <%= lucide_icon("plus", class: "w-5 h-5") %> + <%= t('.new_account') %> + <% end %> +
+ <% if Current.family.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", data: { turbo_frame: "modal" } do %> + <%= lucide_icon("plus", class: "w-5 h-5") %> + <%= t('.new_account') %> + <% end %>
-
- <%= to_accountable_title(account.accountable) %> -
-

- <%= format_currency account.converted_balance %> -

-<% end %> + <% else %> +
+ <% Current.family.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_currency accounts.sum(&:balance) %>

+
+
+ <% accounts.each do |account| %> + <%= render account %> + <% end %> +
+
+ <% end %> +
+
+ <% end %> +
+ <% end %> +
diff --git a/app/views/valuations/create.turbo_stream.erb b/app/views/valuations/create.turbo_stream.erb index 0e0f5e8b..053eb0f3 100644 --- a/app/views/valuations/create.turbo_stream.erb +++ b/app/views/valuations/create.turbo_stream.erb @@ -1,4 +1,4 @@ <%= turbo_stream.replace Valuation.new, body: turbo_frame_tag(dom_id(Valuation.new)) %> <%= turbo_stream.append "notification-tray", partial: "shared/notification", locals: { type: "success", content: "Valuation created" } %> -<%= turbo_stream.replace "valuations_list", partial: "accounts/account_valuation_list", locals: { valuation_series: @account.valuation_series, classification: @account.classification } %> +<%= turbo_stream.replace "valuations_list", partial: "accounts/account_valuation_list", locals: { valuation_series: @account.valuations.to_series(@account) } %> <%= turbo_stream.replace "sync_message", partial: "accounts/sync_message", locals: { is_syncing: true } %> diff --git a/app/views/valuations/destroy.turbo_stream.erb b/app/views/valuations/destroy.turbo_stream.erb index 26f32869..4184dc4c 100644 --- a/app/views/valuations/destroy.turbo_stream.erb +++ b/app/views/valuations/destroy.turbo_stream.erb @@ -1,4 +1,4 @@ <%= turbo_stream.remove @valuation %> <%= turbo_stream.append "notification-tray", partial: "shared/notification", locals: { type: "success", content: "Valuation deleted" } %> -<%= turbo_stream.replace "valuations_list", partial: "accounts/account_valuation_list", locals: { valuation_series: @account.valuation_series, classification: @account.classification } %> +<%= turbo_stream.replace "valuations_list", partial: "accounts/account_valuation_list", locals: { valuation_series: @account.valuations.to_series(@account) } %> <%= turbo_stream.replace "sync_message", partial: "accounts/sync_message", locals: { is_syncing: true } %> diff --git a/config/locales/views/account/en.yml b/config/locales/views/account/en.yml index ff38c58a..932f3df3 100644 --- a/config/locales/views/account/en.yml +++ b/config/locales/views/account/en.yml @@ -4,10 +4,12 @@ en: create: success: New account created successfully index: - title: Cash + new_account: New account new: name: label: Account name placeholder: Example account name select_accountable_type: What would you like to add? title: Add an account + update: + success: Account updated successfully diff --git a/db/migrate/20240306193345_add_is_active_to_account.rb b/db/migrate/20240306193345_add_is_active_to_account.rb new file mode 100644 index 00000000..0da9625b --- /dev/null +++ b/db/migrate/20240306193345_add_is_active_to_account.rb @@ -0,0 +1,5 @@ +class AddIsActiveToAccount < ActiveRecord::Migration[7.2] + def change + add_column :accounts, :is_active, :boolean, default: true, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 564b9cf3..fbd2e521 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_03_02_145715) do +ActiveRecord::Schema[7.2].define(version: 2024_03_06_193345) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -79,7 +79,8 @@ ActiveRecord::Schema[7.2].define(version: 2024_03_02_145715) do t.decimal "converted_balance", precision: 19, scale: 4, default: "0.0" t.string "converted_currency", default: "USD" t.string "status", default: "OK" - t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY ((ARRAY['Account::Loan'::character varying, 'Account::Credit'::character varying, 'Account::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[('Account::Loan'::character varying)::text, ('Account::Credit'::character varying)::text, ('Account::OtherLiability'::character varying)::text])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true + t.boolean "is_active", default: true, null: false t.index ["accountable_type"], name: "index_accounts_on_accountable_type" t.index ["family_id"], name: "index_accounts_on_family_id" end diff --git a/test/models/family_test.rb b/test/models/family_test.rb index 01a2144f..7db5ea75 100644 --- a/test/models/family_test.rb +++ b/test/models/family_test.rb @@ -42,6 +42,22 @@ class FamilyTest < ActiveSupport::TestCase assert_equal BigDecimal("24550"), @family.net_worth end + test "should exclude disabled accounts from calculations" do + assets_before = @family.assets + liabilities_before = @family.liabilities + net_worth_before = @family.net_worth + + disabled_checking = accounts(:checking) + disabled_cc = accounts(:credit_card) + + disabled_checking.update!(is_active: false) + disabled_cc.update!(is_active: false) + + assert_equal assets_before - disabled_checking.balance, @family.assets + assert_equal liabilities_before - disabled_cc.balance, @family.liabilities + assert_equal net_worth_before - disabled_checking.balance + disabled_cc.balance, @family.net_worth + end + test "calculates asset series" do # Sum of expected balances for all asset accounts in balance_calculator_test.rb expected_balances = [ @@ -108,6 +124,23 @@ class FamilyTest < ActiveSupport::TestCase ) end + test "calculates balances by type with disabled account" do + disabled_checking = accounts(:checking).update!(is_active: false) + + verify_balances_by_type( + period: Period.all, + expected_asset_total: BigDecimal("20550"), + expected_liability_total: BigDecimal("1000"), + expected_asset_groups: { + "Account::OtherAsset" => { end_balance: BigDecimal("550"), start_balance: BigDecimal("400"), allocation: 2.68 }, + "Account::Depository" => { end_balance: BigDecimal("20000"), start_balance: BigDecimal("21250"), allocation: 97.32 } + }, + expected_liability_groups: { + "Account::Credit" => { end_balance: BigDecimal("1000"), start_balance: BigDecimal("1040"), allocation: 100 } + } + ) + end + private def verify_balances_by_type(period:, expected_asset_total:, expected_liability_total:, expected_asset_groups:, expected_liability_groups:)