mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-18 20:59:39 +02:00
Add institution management and account editing controls (#868)
* Add institution management * Allow user to select institution on create or edit * Improve redirect behavior * Final cleanup * i18n normalization
This commit is contained in:
parent
8c1a7af37f
commit
9956a9540e
36 changed files with 456 additions and 68 deletions
|
@ -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
|
||||
|
|
35
app/controllers/institutions_controller.rb
Normal file
35
app/controllers/institutions_controller.rb
Normal file
|
@ -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
|
5
app/helpers/institutions_helper.rb
Normal file
5
app/helpers/institutions_helper.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
module InstitutionsHelper
|
||||
def institution_logo(institution)
|
||||
institution.logo.attached? ? institution.logo : institution.logo_url
|
||||
end
|
||||
end
|
|
@ -8,7 +8,7 @@ export default class extends Controller {
|
|||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.imagePreviewTarget.innerHTML = `<img src="${e.target.result}" alt="Preview" class="w-24 h-24 rounded-full object-cover" />`;
|
||||
this.imagePreviewTarget.innerHTML = `<img src="${e.target.result}" alt="Preview" class="w-full h-full rounded-full object-cover" />`;
|
||||
this.templateTarget.classList.add("hidden");
|
||||
this.clearBtnTarget.classList.remove("hidden");
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
7
app/models/institution.rb
Normal file
7
app/models/institution.rb
Normal file
|
@ -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
|
|
@ -1,10 +1,14 @@
|
|||
<%= turbo_frame_tag dom_id(account) do %>
|
||||
<div class="p-4 flex items-center justify-between gap-3">
|
||||
<div class="p-4 flex items-center justify-between gap-3 group/account">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 flex items-center justify-center rounded-full text-xs font-medium <%= account.is_active ? "bg-blue-500/10 text-blue-500" : "bg-gray-500/10 text-gray-500" %>">
|
||||
<%= account.name[0].upcase %>
|
||||
</div>
|
||||
<%= 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 %>
|
||||
</div>
|
||||
<div class="flex items-center gap-8">
|
||||
<p class="text-sm font-medium <%= account.is_active ? "text-gray-900" : "text-gray-400" %>">
|
||||
|
|
|
@ -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 %>
|
||||
<span class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg <%= bg_color %> border border-alpha-black-25">
|
||||
<%= lucide_icon(icon, class: "#{text_color} w-5 h-5") %>
|
||||
</span>
|
||||
|
|
17
app/views/accounts/_accountable_group.html.erb
Normal file
17
app/views/accounts/_accountable_group.html.erb
Normal file
|
@ -0,0 +1,17 @@
|
|||
<%# locals: (accounts:) %>
|
||||
|
||||
<% accounts.group_by(&:accountable_type).each do |group, accounts| %>
|
||||
<div class="bg-gray-25 p-1 rounded-xl">
|
||||
<div class="flex items-center px-4 py-2 text-xs font-medium text-gray-500">
|
||||
<p><%= to_accountable_title(Accountable.from_type(group)) %></p>
|
||||
<span class="text-gray-400 mx-2">·</span>
|
||||
<p><%= accounts.count %></p>
|
||||
<p class="ml-auto"><%= format_money accounts.sum(&:balance_money) %></p>
|
||||
</div>
|
||||
<div class="bg-white">
|
||||
<% accounts.each do |account| %>
|
||||
<%= render account %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
11
app/views/accounts/_empty.html.erb
Normal file
11
app/views/accounts/_empty.html.erb
Normal file
|
@ -0,0 +1,11 @@
|
|||
<div class="flex justify-center items-center h-[800px] text-sm">
|
||||
<div class="text-center flex flex-col items-center max-w-[300px]">
|
||||
<%= 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") %>
|
||||
<span><%= t(".new_account") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
|
@ -6,7 +6,7 @@
|
|||
<%= text %>
|
||||
</span>
|
||||
<% 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 %>
|
||||
<span class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-alpha-black-50 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.02)]">
|
||||
<%= lucide_icon(icon, class: "text-gray-500 w-5 h-5") %>
|
||||
</span>
|
||||
|
|
69
app/views/accounts/_institution_accounts.html.erb
Normal file
69
app/views/accounts/_institution_accounts.html.erb
Normal file
|
@ -0,0 +1,69 @@
|
|||
<%# locals: (institution:) %>
|
||||
|
||||
<details open class="group bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
|
||||
<summary class="flex items-center gap-2 focus-visible:outline-none">
|
||||
<%= lucide_icon "chevron-right", class: "group-open:transform group-open:rotate-90 text-gray-500 w-5" %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-blue-600/10 rounded-full bg-black/5">
|
||||
<% if institution_logo(institution) %>
|
||||
<%= image_tag institution_logo(institution), class: "rounded-full h-full w-full" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p institution.name.first.upcase, class: "text-blue-600 text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= 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 %>
|
||||
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= 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" %>
|
||||
|
||||
<span><%= t(".add_account_to_institution") %></span>
|
||||
<% 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" %>
|
||||
|
||||
<span><%= t(".edit") %></span>
|
||||
<% 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" %>
|
||||
|
||||
<span><%= t(".delete") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% end %>
|
||||
</summary>
|
||||
|
||||
<div class="space-y-4 mt-4">
|
||||
<% if institution.accounts.any? %>
|
||||
<%= render "accountable_group", accounts: institution.accounts %>
|
||||
<% else %>
|
||||
<div class="p-4 flex flex-col gap-3 items-center justify-center">
|
||||
<p class="text-gray-500 text-sm">There are no accounts in this financial institution</p>
|
||||
<%= 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") %>
|
||||
<span><%= t(".new_account") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</details>
|
17
app/views/accounts/_institutionless_accounts.html.erb
Normal file
17
app/views/accounts/_institutionless_accounts.html.erb
Normal file
|
@ -0,0 +1,17 @@
|
|||
<%# locals: (accounts:) %>
|
||||
|
||||
<details open class="group bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
|
||||
<summary class="flex items-center gap-2 focus-visible:outline-none">
|
||||
<%= lucide_icon "chevron-right", class: "group-open:transform group-open:rotate-90 text-gray-500 w-5" %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 rounded-full bg-black/5">
|
||||
<%= lucide_icon("folder-pen", class: "w-5 h-5 text-gray-500") %>
|
||||
</div>
|
||||
|
||||
<span class="mr-auto text-sm font-medium text-gray-900"><%= t(".other_accounts") %></span>
|
||||
</summary>
|
||||
|
||||
<div class="space-y-4 mt-4">
|
||||
<%= render "accountable_group", accounts: accounts %>
|
||||
</div>
|
||||
</details>
|
21
app/views/accounts/edit.html.erb
Normal file
21
app/views/accounts/edit.html.erb
Normal file
|
@ -0,0 +1,21 @@
|
|||
<%= modal do %>
|
||||
<article class="mx-auto w-full p-4 space-y-4 min-w-[350px]">
|
||||
<header class="flex justify-between">
|
||||
<h2 class="font-medium text-xl"><%= t(".edit", account: @account.name) %></h2>
|
||||
<%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %>
|
||||
</header>
|
||||
|
||||
<%= form_with model: @account, data: { turbo_frame: "_top" } do |f| %>
|
||||
<%= f.text_field :name, label: "Name" %>
|
||||
|
||||
<div class="relative">
|
||||
<%= 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 %>
|
||||
</div>
|
||||
|
||||
<%= f.submit %>
|
||||
<% end %>
|
||||
</article>
|
||||
<% end %>
|
|
@ -1,62 +1,45 @@
|
|||
<% content_for :sidebar do %>
|
||||
<%= render "settings/nav" %>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-medium text-gray-900">Accounts</h1>
|
||||
<%= 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") %>
|
||||
<span><%= t(".new_account") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if @accounts.empty? %>
|
||||
<div class="flex justify-center items-center h-[800px] text-sm">
|
||||
<div class="text-center flex flex-col items-center max-w-[300px]">
|
||||
<p class="text-gray-900 mb-1 font-medium">No accounts yet</p>
|
||||
<p class="text-gray-500 mb-4">Add an account either via connection, importing or entering manually.</p>
|
||||
<%= 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 %>
|
||||
<header class="flex justify-between items-center text-gray-900 font-medium">
|
||||
<h1 class="text-xl"><%= t(".accounts") %></h1>
|
||||
<div class="flex items-center gap-5">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= contextual_menu do %>
|
||||
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= 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" %>
|
||||
<span class="text-black"><%= t(".add_institution") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% 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") %>
|
||||
<span><%= t(".new_account") %></span>
|
||||
<p class="text-sm font-medium"><%= t(".new_account") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<% if @accounts.empty? && @institutions.empty? %>
|
||||
<%= render "empty" %>
|
||||
<% else %>
|
||||
<div>
|
||||
<% @accounts.by_provider.each do |item| %>
|
||||
<details open class="bg-white group p-4 border border-alpha-black-25 shadow-xs rounded-xl">
|
||||
<summary class="flex items-center gap-2">
|
||||
<%= 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" %>
|
||||
<div class="flex items-center justify-center h-8 w-8 rounded-full bg-black/5">
|
||||
<%= lucide_icon("folder-pen", class: "w-5 h-5 text-gray-500") %>
|
||||
</div>
|
||||
<% end %>
|
||||
<span class="text-sm font-medium text-gray-900">
|
||||
<%= item[:name] %>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="space-y-4 mt-4">
|
||||
<% item[:accounts].each do |group, accounts| %>
|
||||
<div class="bg-gray-25 p-1 rounded-xl">
|
||||
<div class="flex items-center px-4 py-2 text-xs font-medium text-gray-500">
|
||||
<p><%= to_accountable_title(Accountable.from_type(group)) %></p>
|
||||
<span class="text-gray-400 mx-2">·</span>
|
||||
<p><%= accounts.count %></p>
|
||||
<p class="ml-auto"><%= format_money accounts.sum(&:balance_money) %></p>
|
||||
</div>
|
||||
<div class="bg-white">
|
||||
<% accounts.each do |account| %>
|
||||
<%= render account %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</details>
|
||||
<div class="space-y-2">
|
||||
<% @institutions.each do |institution| %>
|
||||
<%= render "institution_accounts", institution: %>
|
||||
<% end %>
|
||||
|
||||
<%= render "institutionless_accounts", accounts: @accounts %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="flex justify-between gap-4">
|
||||
<% if self_hosted? %>
|
||||
<%= previous_setting("Self-Hosting", settings_hosting_path) %>
|
||||
|
|
|
@ -76,6 +76,7 @@
|
|||
<div class="space-y-4 grow">
|
||||
<%= 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" %>
|
||||
|
||||
<div class="hidden peer-checked:flex items-center gap-2 mt-3">
|
||||
<div class="hidden peer-checked:flex items-center gap-2 mt-3 mb-6">
|
||||
<div class="w-1/2"><%= f.date_field :start_date, label: t(".start_date"), max: Date.current %></div>
|
||||
<div class="w-1/2"><%= f.number_field :start_balance, label: t(".start_balance") %></div>
|
||||
</div>
|
||||
|
|
|
@ -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 %>
|
||||
<div class="relative cursor-not-allowed">
|
||||
<div class="flex items-center gap-2 px-3 py-2">
|
||||
<span class="text-gray-900"><%= @account.balance_money.currency.iso_code %> <%= @account.balance_money.currency.symbol %></span>
|
||||
<%= lucide_icon("chevron-down", class: "w-5 h-5 text-gray-500") %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= contextual_menu do %>
|
||||
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= 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" %>
|
||||
|
||||
<span><%= t(".edit") %></span>
|
||||
<% 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" %>
|
||||
|
|
27
app/views/institutions/_form.html.erb
Normal file
27
app/views/institutions/_form.html.erb
Normal file
|
@ -0,0 +1,27 @@
|
|||
<%= form_with model: institution, data: { turbo_frame: "_top", controller: "profile-image-preview" } do |f| %>
|
||||
|
||||
<div class="flex justify-center items-center py-4">
|
||||
<%= f.label :logo do %>
|
||||
<div class="relative cursor-pointer hover:opacity-80 w-16 h-16 rounded-full bg-gray-50">
|
||||
<% 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 %>
|
||||
|
||||
<div data-profile-image-preview-target="imagePreview" class="absolute inset-0 h-full w-full flex items-center justify-center">
|
||||
<% unless persisted_logo %>
|
||||
<%= lucide_icon "image-plus", class: "w-5 h-5 text-gray-500 cursor-pointer", data: { profile_image_preview_target: "template" } %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= 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 %>
|
10
app/views/institutions/edit.html.erb
Normal file
10
app/views/institutions/edit.html.erb
Normal file
|
@ -0,0 +1,10 @@
|
|||
<%= modal do %>
|
||||
<article class="mx-auto w-full p-4 space-y-4 min-w-[350px]">
|
||||
<header class="flex justify-between">
|
||||
<h2 class="font-medium text-xl"><%= t(".edit", institution: @institution.name) %></h2>
|
||||
<%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %>
|
||||
</header>
|
||||
|
||||
<%= render "form", institution: @institution %>
|
||||
</article>
|
||||
<% end %>
|
10
app/views/institutions/new.html.erb
Normal file
10
app/views/institutions/new.html.erb
Normal file
|
@ -0,0 +1,10 @@
|
|||
<%= modal do %>
|
||||
<article class="mx-auto w-full p-4 space-y-4 min-w-[350px]">
|
||||
<header class="flex justify-between">
|
||||
<h2 class="font-medium text-xl"><%= t(".new_institution") %></h2>
|
||||
<%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %>
|
||||
</header>
|
||||
|
||||
<%= render "form", institution: @institution %>
|
||||
</article>
|
||||
<% end %>
|
|
@ -8,7 +8,7 @@
|
|||
<%= form_with model: Current.user, url: settings_profile_path, html: {data: { controller: "profile-image-preview" }} do |form| %>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative flex justify-center items-center bg-gray-50 w-24 h-24 rounded-full border border-alpha-black-25">
|
||||
<div data-profile-image-preview-target="imagePreview">
|
||||
<div data-profile-image-preview-target="imagePreview" class="h-full w-full flex justify-center items-center">
|
||||
<% profile_image_attached = Current.user.profile_image.attached? %>
|
||||
<% if profile_image_attached %>
|
||||
<div class="h-24 w-24">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<%# locals: (content:, classes:) -%>
|
||||
<%= turbo_frame_tag "modal" do %>
|
||||
<dialog class="bg-white border border-alpha-black-25 rounded-2xl max-h-[648px] max-w-[580px] w-min-content shadow-xs h-fit <%= classes %>" data-controller="modal" data-action="click->modal#clickOutside">
|
||||
<dialog class="bg-white border border-alpha-black-25 rounded-2xl max-w-[580px] w-min-content shadow-xs h-fit <%= classes %>" data-controller="modal" data-action="click->modal#clickOutside">
|
||||
<div class="flex flex-col">
|
||||
<%= content %>
|
||||
</div>
|
||||
|
|
|
@ -12,17 +12,40 @@ en:
|
|||
success: New account created successfully
|
||||
destroy:
|
||||
success: Account deleted successfully
|
||||
edit:
|
||||
edit: Edit %{account}
|
||||
institution: Financial institution
|
||||
ungrouped: "(none)"
|
||||
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:
|
||||
add_account_to_institution: Add new account
|
||||
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: Current balance
|
||||
currency:
|
||||
all_others: All Others
|
||||
popular: Popular
|
||||
institution: Financial institution
|
||||
name:
|
||||
label: Account name
|
||||
placeholder: Example account name
|
||||
|
@ -31,6 +54,7 @@ en:
|
|||
start_balance: Start balance (optional)
|
||||
start_date: Start date (optional)
|
||||
title: Add an account
|
||||
ungrouped: "(none)"
|
||||
show:
|
||||
confirm_accept: Delete "%{name}"
|
||||
confirm_body_html: "<p>By deleting this account, you will erase its value history,
|
||||
|
@ -39,6 +63,7 @@ en:
|
|||
/> <p>After deletion, there is no way you'll be able to restore the account
|
||||
information because you'll need to add it as a new account.</p>"
|
||||
confirm_title: Delete account?
|
||||
edit: Edit
|
||||
import: Import transactions
|
||||
sync_message_missing_rates: Since exchange rates haven't been synced, balance
|
||||
graphs may not reflect accurate values.
|
||||
|
|
15
config/locales/views/institutions/en.yml
Normal file
15
config/locales/views/institutions/en.yml
Normal file
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
en:
|
||||
institutions:
|
||||
create:
|
||||
success: Institution created
|
||||
destroy:
|
||||
success: Institution deleted
|
||||
edit:
|
||||
edit: Edit %{institution}
|
||||
form:
|
||||
name: Financial institution name
|
||||
new:
|
||||
new_institution: New financial institution
|
||||
update:
|
||||
success: Institution updated
|
|
@ -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
|
||||
|
|
11
db/migrate/20240612164751_create_institutions.rb
Normal file
11
db/migrate/20240612164751_create_institutions.rb
Normal file
|
@ -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
|
5
db/migrate/20240612164944_add_institution_to_accounts.rb
Normal file
5
db/migrate/20240612164944_add_institution_to_accounts.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
class AddInstitutionToAccounts < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
add_reference :accounts, :institution, foreign_key: true, type: :uuid
|
||||
end
|
||||
end
|
15
db/schema.rb
generated
15
db/schema.rb
generated
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
@ -19,7 +28,9 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest
|
|||
test "should update account" do
|
||||
patch account_url(@account), params: {
|
||||
account: {
|
||||
is_active: "0"
|
||||
name: "Updated name",
|
||||
is_active: "0",
|
||||
institution_id: institutions(:chase).id
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,7 +44,8 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest
|
|||
account: {
|
||||
accountable_type: "Account::Depository",
|
||||
balance: 200,
|
||||
subtype: "checking"
|
||||
subtype: "checking",
|
||||
institution_id: institutions(:chase).id
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,6 +61,7 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest
|
|||
accountable_type: "Account::Depository",
|
||||
balance: 200,
|
||||
subtype: "checking",
|
||||
institution_id: institutions(:chase).id,
|
||||
start_balance: 100,
|
||||
start_date: 10.days.ago
|
||||
}
|
||||
|
|
55
test/controllers/institutions_controller_test.rb
Normal file
55
test/controllers/institutions_controller_test.rb
Normal file
|
@ -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
|
5
test/fixtures/accounts.yml
vendored
5
test/fixtures/accounts.yml
vendored
|
@ -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
|
||||
|
|
4
test/fixtures/active_storage/attachments.yml
vendored
Normal file
4
test/fixtures/active_storage/attachments.yml
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
chase_logo_attachment:
|
||||
name: logo
|
||||
record: chase (Institution)
|
||||
blob: square_placeholder_blob
|
1
test/fixtures/active_storage/blobs.yml
vendored
Normal file
1
test/fixtures/active_storage/blobs.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
square_placeholder_blob: <%= ActiveStorage::FixtureSet.blob filename: "square-placeholder.png" %>
|
BIN
test/fixtures/files/square-placeholder.png
vendored
Normal file
BIN
test/fixtures/files/square-placeholder.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.4 KiB |
8
test/fixtures/institutions.yml
vendored
Normal file
8
test/fixtures/institutions.yml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
chase:
|
||||
name: Chase
|
||||
family: dylan_family
|
||||
|
||||
revolut:
|
||||
name: Revolut
|
||||
family: dylan_family
|
||||
logo_url: <%= "file://" + Rails.root.join('test/fixtures/files/square-placeholder.png').to_s %>
|
Loading…
Add table
Add a link
Reference in a new issue