diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 9fabaebc..94e9bbe3 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -2,11 +2,12 @@ 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
- @accounts = Current.family.accounts
+ @institutions = Current.family.institutions
+ @accounts = Current.family.accounts.ungrouped.alphabetically
end
def summary
@@ -26,6 +27,10 @@ class AccountsController < ApplicationController
balance: nil,
accountable: Accountable.from_type(params[:type])&.new
)
+
+ if params[:institution_id]
+ @account.institution = Current.family.institutions.find_by(id: params[:institution_id])
+ end
end
def show
@@ -33,6 +38,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")
@@ -46,7 +54,7 @@ class AccountsController < ApplicationController
start_date: account_params[:start_date],
start_balance: account_params[:start_balance]
- redirect_to account_path(@account), notice: t(".success")
+ redirect_back_or_to account_path(@account), notice: t(".success")
end
def destroy
@@ -80,7 +88,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/controllers/institutions_controller.rb b/app/controllers/institutions_controller.rb
new file mode 100644
index 00000000..92e9e63d
--- /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: t(".success")
+ end
+
+ def edit
+ end
+
+ def update
+ @institution.update!(institution_params)
+ redirect_to accounts_path, notice: t(".success")
+ end
+
+ def destroy
+ @institution.destroy!
+ redirect_to accounts_path, notice: t(".success")
+ 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 = `
`;
+ this.imagePreviewTarget.innerHTML = `
`;
this.templateTarget.classList.add("hidden");
this.clearBtnTarget.classList.remove("hidden");
};
diff --git a/app/models/account.rb b/app/models/account.rb
index a41e9b3f..dee4cae7 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..3d9f13db
--- /dev/null
+++ b/app/models/institution.rb
@@ -0,0 +1,7 @@
+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
new file mode 100644
index 00000000..199c8fe1
--- /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 %>
diff --git a/app/views/accounts/_empty.html.erb b/app/views/accounts/_empty.html.erb
new file mode 100644
index 00000000..e938c863
--- /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 %>
+
+
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
new file mode 100644
index 00000000..ee50e3bb
--- /dev/null
+++ b/app/views/accounts/_institution_accounts.html.erb
@@ -0,0 +1,69 @@
+<%# 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 new_account_path(institution_id: institution.id),
+ 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 "plus", class: "w-5 h-5 text-gray-500" %>
+
+ <%= t(".add_account_to_institution") %>
+ <% end %>
+
+ <%= 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(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 %>
+
+
diff --git a/app/views/accounts/_institutionless_accounts.html.erb b/app/views/accounts/_institutionless_accounts.html.erb
new file mode 100644
index 00000000..335e842e
--- /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 %>
+
+
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 914dcfe7..9c5a88b0 100644
--- a/app/views/accounts/index.html.erb
+++ b/app/views/accounts/index.html.erb
@@ -1,62 +1,45 @@
<% 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/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" %>
-
+
<%= f.date_field :start_date, label: t(".start_date"), max: Date.current %>
<%= f.number_field :start_balance, label: t(".start_balance") %>
diff --git a/app/views/accounts/show.html.erb b/app/views/accounts/show.html.erb
index 15b8c2f5..1e5187e3 100644
--- a/app/views/accounts/show.html.erb
+++ b/app/views/accounts/show.html.erb
@@ -9,15 +9,17 @@
<%= button_to sync_account_path(@account), method: :post, class: "flex items-center gap-2", title: "Sync Account" do %>
<%= lucide_icon "refresh-cw", class: "w-4 h-4 text-gray-900 hover:text-gray-500" %>
<% end %>
-
-
- <%= @account.balance_money.currency.iso_code %> <%= @account.balance_money.currency.symbol %>
- <%= lucide_icon("chevron-down", class: "w-5 h-5 text-gray-500") %>
-
-
<%= contextual_menu do %>
+ <%= link_to edit_account_path(@account),
+ data: { turbo_frame: :modal },
+ class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg" do %>
+ <%= lucide_icon "pencil-line", class: "w-5 h-5 text-gray-500" %>
+
+
<%= t(".edit") %>
+ <% end %>
+
<%= link_to new_import_path(account_id: @account.id),
class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg" do %>
<%= lucide_icon "download", class: "w-5 h-5 text-gray-500" %>
diff --git a/app/views/institutions/_form.html.erb b/app/views/institutions/_form.html.erb
new file mode 100644
index 00000000..7cf47892
--- /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 %>
diff --git a/app/views/institutions/edit.html.erb b/app/views/institutions/edit.html.erb
new file mode 100644
index 00000000..fbcb813c
--- /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 %>
diff --git a/app/views/institutions/new.html.erb b/app/views/institutions/new.html.erb
new file mode 100644
index 00000000..7f74dac2
--- /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 %>
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/app/views/shared/_modal.html.erb b/app/views/shared/_modal.html.erb
index 7d6ae1d1..18dea29d 100644
--- a/app/views/shared/_modal.html.erb
+++ b/app/views/shared/_modal.html.erb
@@ -1,6 +1,6 @@
<%# locals: (content:, classes:) -%>
<%= turbo_frame_tag "modal" do %>
-