mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 21:29:38 +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"
|
layout "with_sidebar"
|
||||||
|
|
||||||
include Filterable
|
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
|
after_action :sync_account, only: :create
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@accounts = Current.family.accounts
|
@institutions = Current.family.institutions
|
||||||
|
@accounts = Current.family.accounts.ungrouped.alphabetically
|
||||||
end
|
end
|
||||||
|
|
||||||
def summary
|
def summary
|
||||||
|
@ -26,6 +27,10 @@ class AccountsController < ApplicationController
|
||||||
balance: nil,
|
balance: nil,
|
||||||
accountable: Accountable.from_type(params[:type])&.new
|
accountable: Accountable.from_type(params[:type])&.new
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if params[:institution_id]
|
||||||
|
@account.institution = Current.family.institutions.find_by(id: params[:institution_id])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
@ -33,6 +38,9 @@ class AccountsController < ApplicationController
|
||||||
@valuation_series = @account.valuations.to_series
|
@valuation_series = @account.valuations.to_series
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@account.update! account_params.except(:accountable_type)
|
@account.update! account_params.except(:accountable_type)
|
||||||
redirect_back_or_to account_path(@account), notice: t(".success")
|
redirect_back_or_to account_path(@account), notice: t(".success")
|
||||||
|
@ -46,7 +54,7 @@ class AccountsController < ApplicationController
|
||||||
start_date: account_params[:start_date],
|
start_date: account_params[:start_date],
|
||||||
start_balance: account_params[:start_balance]
|
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
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
@ -80,7 +88,7 @@ class AccountsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_params
|
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
|
end
|
||||||
|
|
||||||
def sync_account
|
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) {
|
if (file) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
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.templateTarget.classList.add("hidden");
|
||||||
this.clearBtnTarget.classList.remove("hidden");
|
this.clearBtnTarget.classList.remove("hidden");
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,10 +2,12 @@ class Account < ApplicationRecord
|
||||||
include Syncable
|
include Syncable
|
||||||
include Monetizable
|
include Monetizable
|
||||||
|
|
||||||
|
broadcasts_refreshes
|
||||||
|
|
||||||
validates :family, presence: true
|
validates :family, presence: true
|
||||||
|
|
||||||
broadcasts_refreshes
|
|
||||||
belongs_to :family
|
belongs_to :family
|
||||||
|
belongs_to :institution, optional: true
|
||||||
has_many :balances, dependent: :destroy
|
has_many :balances, dependent: :destroy
|
||||||
has_many :valuations, dependent: :destroy
|
has_many :valuations, dependent: :destroy
|
||||||
has_many :transactions, dependent: :destroy
|
has_many :transactions, dependent: :destroy
|
||||||
|
@ -19,6 +21,7 @@ class Account < ApplicationRecord
|
||||||
scope :assets, -> { where(classification: "asset") }
|
scope :assets, -> { where(classification: "asset") }
|
||||||
scope :liabilities, -> { where(classification: "liability") }
|
scope :liabilities, -> { where(classification: "liability") }
|
||||||
scope :alphabetically, -> { order(:name) }
|
scope :alphabetically, -> { order(:name) }
|
||||||
|
scope :ungrouped, -> { where(institution_id: nil) }
|
||||||
|
|
||||||
delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy
|
delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ class Family < ApplicationRecord
|
||||||
has_many :users, dependent: :destroy
|
has_many :users, dependent: :destroy
|
||||||
has_many :tags, dependent: :destroy
|
has_many :tags, dependent: :destroy
|
||||||
has_many :accounts, dependent: :destroy
|
has_many :accounts, dependent: :destroy
|
||||||
|
has_many :institutions, dependent: :destroy
|
||||||
has_many :transactions, through: :accounts
|
has_many :transactions, through: :accounts
|
||||||
has_many :imports, through: :accounts
|
has_many :imports, through: :accounts
|
||||||
has_many :transaction_categories, dependent: :destroy, class_name: "Transaction::Category"
|
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 %>
|
<%= 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="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" %>">
|
<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 %>
|
<%= account.name[0].upcase %>
|
||||||
</div>
|
</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 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>
|
||||||
<div class="flex items-center gap-8">
|
<div class="flex items-center gap-8">
|
||||||
<p class="text-sm font-medium <%= account.is_active ? "text-gray-900" : "text-gray-400" %>">
|
<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">
|
<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") %>
|
<%= lucide_icon(icon, class: "#{text_color} w-5 h-5") %>
|
||||||
</span>
|
</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 %>
|
<%= text %>
|
||||||
</span>
|
</span>
|
||||||
<% else %>
|
<% 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)]">
|
<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") %>
|
<%= lucide_icon(icon, class: "text-gray-500 w-5 h-5") %>
|
||||||
</span>
|
</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 %>
|
<% content_for :sidebar do %>
|
||||||
<%= render "settings/nav" %>
|
<%= render "settings/nav" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="flex items-center justify-between">
|
<header class="flex justify-between items-center text-gray-900 font-medium">
|
||||||
<h1 class="text-xl font-medium text-gray-900">Accounts</h1>
|
<h1 class="text-xl"><%= t(".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 %>
|
<div class="flex items-center gap-5">
|
||||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
<div class="flex items-center gap-2">
|
||||||
<span><%= t(".new_account") %></span>
|
<%= 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 %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% if @accounts.empty? %>
|
<% end %>
|
||||||
<div class="flex justify-center items-center h-[800px] text-sm">
|
|
||||||
<div class="text-center flex flex-col items-center max-w-[300px]">
|
<%= link_to new_account_path,
|
||||||
<p class="text-gray-900 mb-1 font-medium">No accounts yet</p>
|
data: { turbo_frame: "modal" },
|
||||||
<p class="text-gray-500 mb-4">Add an account either via connection, importing or entering manually.</p>
|
class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2" do %>
|
||||||
<%= 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") %>
|
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||||
<span><%= t(".new_account") %></span>
|
<p class="text-sm font-medium"><%= t(".new_account") %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<% if @accounts.empty? && @institutions.empty? %>
|
||||||
|
<%= render "empty" %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div>
|
<div class="space-y-2">
|
||||||
<% @accounts.by_provider.each do |item| %>
|
<% @institutions.each do |institution| %>
|
||||||
<details open class="bg-white group p-4 border border-alpha-black-25 shadow-xs rounded-xl">
|
<%= render "institution_accounts", institution: %>
|
||||||
<summary class="flex items-center gap-2">
|
<% end %>
|
||||||
<%= 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") %>
|
<%= render "institutionless_accounts", accounts: @accounts %>
|
||||||
<% 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>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="flex justify-between gap-4">
|
<div class="flex justify-between gap-4">
|
||||||
<% if self_hosted? %>
|
<% if self_hosted? %>
|
||||||
<%= previous_setting("Self-Hosting", settings_hosting_path) %>
|
<%= previous_setting("Self-Hosting", settings_hosting_path) %>
|
||||||
|
|
|
@ -76,6 +76,7 @@
|
||||||
<div class="space-y-4 grow">
|
<div class="space-y-4 grow">
|
||||||
<%= f.hidden_field :accountable_type %>
|
<%= f.hidden_field :accountable_type %>
|
||||||
<%= f.text_field :name, placeholder: t(".name.placeholder"), required: "required", label: t(".name.label"), autofocus: true %>
|
<%= 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 %>
|
<%= render "accounts/#{permitted_accountable_partial(@account.accountable_type)}", f: f %>
|
||||||
<%= f.money_field :balance_money, label: t(".balance"), required: "required" %>
|
<%= 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" %>
|
<%= 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" %>
|
<%= 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.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 class="w-1/2"><%= f.number_field :start_balance, label: t(".start_balance") %></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,15 +9,17 @@
|
||||||
<%= button_to sync_account_path(@account), method: :post, class: "flex items-center gap-2", title: "Sync Account" do %>
|
<%= 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" %>
|
<%= lucide_icon "refresh-cw", class: "w-4 h-4 text-gray-900 hover:text-gray-500" %>
|
||||||
<% end %>
|
<% 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 %>
|
<%= 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">
|
<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),
|
<%= 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 %>
|
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" %>
|
<%= 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| %>
|
<%= 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="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 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? %>
|
<% profile_image_attached = Current.user.profile_image.attached? %>
|
||||||
<% if profile_image_attached %>
|
<% if profile_image_attached %>
|
||||||
<div class="h-24 w-24">
|
<div class="h-24 w-24">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<%# locals: (content:, classes:) -%>
|
<%# locals: (content:, classes:) -%>
|
||||||
<%= turbo_frame_tag "modal" do %>
|
<%= 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">
|
<div class="flex flex-col">
|
||||||
<%= content %>
|
<%= content %>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,17 +12,40 @@ en:
|
||||||
success: New account created successfully
|
success: New account created successfully
|
||||||
destroy:
|
destroy:
|
||||||
success: Account deleted successfully
|
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:
|
header:
|
||||||
accounts: Accounts
|
accounts: Accounts
|
||||||
manage: Manage accounts
|
manage: Manage accounts
|
||||||
new: New account
|
new: New account
|
||||||
index:
|
index:
|
||||||
|
accounts: Accounts
|
||||||
|
add_institution: Add institution
|
||||||
new_account: New account
|
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:
|
new:
|
||||||
balance: Current balance
|
balance: Current balance
|
||||||
currency:
|
currency:
|
||||||
all_others: All Others
|
all_others: All Others
|
||||||
popular: Popular
|
popular: Popular
|
||||||
|
institution: Financial institution
|
||||||
name:
|
name:
|
||||||
label: Account name
|
label: Account name
|
||||||
placeholder: Example account name
|
placeholder: Example account name
|
||||||
|
@ -31,6 +54,7 @@ en:
|
||||||
start_balance: Start balance (optional)
|
start_balance: Start balance (optional)
|
||||||
start_date: Start date (optional)
|
start_date: Start date (optional)
|
||||||
title: Add an account
|
title: Add an account
|
||||||
|
ungrouped: "(none)"
|
||||||
show:
|
show:
|
||||||
confirm_accept: Delete "%{name}"
|
confirm_accept: Delete "%{name}"
|
||||||
confirm_body_html: "<p>By deleting this account, you will erase its value history,
|
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
|
/> <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>"
|
information because you'll need to add it as a new account.</p>"
|
||||||
confirm_title: Delete account?
|
confirm_title: Delete account?
|
||||||
|
edit: Edit
|
||||||
import: Import transactions
|
import: Import transactions
|
||||||
sync_message_missing_rates: Since exchange rates haven't been synced, balance
|
sync_message_missing_rates: Since exchange rates haven't been synced, balance
|
||||||
graphs may not reflect accurate values.
|
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
|
resources :valuations
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resources :institutions, except: %i[ index show ]
|
||||||
|
|
||||||
# For managing self-hosted upgrades and release notifications
|
# For managing self-hosted upgrades and release notifications
|
||||||
resources :upgrades, only: [] do
|
resources :upgrades, only: [] do
|
||||||
member 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.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pgcrypto"
|
enable_extension "pgcrypto"
|
||||||
enable_extension "plpgsql"
|
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_warnings", default: [], null: false
|
||||||
t.jsonb "sync_errors", default: [], null: false
|
t.jsonb "sync_errors", default: [], null: false
|
||||||
t.date "last_sync_date"
|
t.date "last_sync_date"
|
||||||
|
t.uuid "institution_id"
|
||||||
t.index ["accountable_type"], name: "index_accounts_on_accountable_type"
|
t.index ["accountable_type"], name: "index_accounts_on_accountable_type"
|
||||||
t.index ["family_id"], name: "index_accounts_on_family_id"
|
t.index ["family_id"], name: "index_accounts_on_family_id"
|
||||||
|
t.index ["institution_id"], name: "index_accounts_on_institution_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "active_storage_attachments", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
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"
|
t.index ["account_id"], name: "index_imports_on_account_id"
|
||||||
end
|
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|
|
create_table "invite_codes", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||||
t.string "token", null: false
|
t.string "token", null: false
|
||||||
t.datetime "created_at", 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 "account_balances", "accounts", on_delete: :cascade
|
||||||
add_foreign_key "accounts", "families"
|
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_attachments", "active_storage_blobs", column: "blob_id"
|
||||||
add_foreign_key "active_storage_variant_records", "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 "imports", "accounts"
|
||||||
|
add_foreign_key "institutions", "families"
|
||||||
add_foreign_key "taggings", "tags"
|
add_foreign_key "taggings", "tags"
|
||||||
add_foreign_key "tags", "families"
|
add_foreign_key "tags", "families"
|
||||||
add_foreign_key "transaction_categories", "families"
|
add_foreign_key "transaction_categories", "families"
|
||||||
|
|
|
@ -6,6 +6,15 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest
|
||||||
@account = accounts(:checking)
|
@account = accounts(:checking)
|
||||||
end
|
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
|
test "new" do
|
||||||
get new_account_path
|
get new_account_path
|
||||||
assert_response :ok
|
assert_response :ok
|
||||||
|
@ -19,7 +28,9 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest
|
||||||
test "should update account" do
|
test "should update account" do
|
||||||
patch account_url(@account), params: {
|
patch account_url(@account), params: {
|
||||||
account: {
|
account: {
|
||||||
is_active: "0"
|
name: "Updated name",
|
||||||
|
is_active: "0",
|
||||||
|
institution_id: institutions(:chase).id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +44,8 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest
|
||||||
account: {
|
account: {
|
||||||
accountable_type: "Account::Depository",
|
accountable_type: "Account::Depository",
|
||||||
balance: 200,
|
balance: 200,
|
||||||
subtype: "checking"
|
subtype: "checking",
|
||||||
|
institution_id: institutions(:chase).id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +61,7 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest
|
||||||
accountable_type: "Account::Depository",
|
accountable_type: "Account::Depository",
|
||||||
balance: 200,
|
balance: 200,
|
||||||
subtype: "checking",
|
subtype: "checking",
|
||||||
|
institution_id: institutions(:chase).id,
|
||||||
start_balance: 100,
|
start_balance: 100,
|
||||||
start_date: 10.days.ago
|
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
|
balance: 5000
|
||||||
accountable_type: Account::Depository
|
accountable_type: Account::Depository
|
||||||
accountable_id: "123e4567-e89b-12d3-a456-426614174000"
|
accountable_id: "123e4567-e89b-12d3-a456-426614174000"
|
||||||
|
institution: chase
|
||||||
|
|
||||||
# Account with both transactions and valuations
|
# Account with both transactions and valuations
|
||||||
savings_with_valuation_overrides:
|
savings_with_valuation_overrides:
|
||||||
|
@ -21,6 +22,7 @@ savings_with_valuation_overrides:
|
||||||
balance: 20000
|
balance: 20000
|
||||||
accountable_type: Account::Depository
|
accountable_type: Account::Depository
|
||||||
accountable_id: "123e4567-e89b-12d3-a456-426614174001"
|
accountable_id: "123e4567-e89b-12d3-a456-426614174001"
|
||||||
|
institution: chase
|
||||||
|
|
||||||
# Liability account
|
# Liability account
|
||||||
credit_card:
|
credit_card:
|
||||||
|
@ -29,6 +31,7 @@ credit_card:
|
||||||
balance: 1000
|
balance: 1000
|
||||||
accountable_type: Account::Credit
|
accountable_type: Account::Credit
|
||||||
accountable_id: "123e4567-e89b-12d3-a456-426614174003"
|
accountable_id: "123e4567-e89b-12d3-a456-426614174003"
|
||||||
|
institution: chase
|
||||||
|
|
||||||
eur_checking:
|
eur_checking:
|
||||||
family: dylan_family
|
family: dylan_family
|
||||||
|
@ -37,6 +40,7 @@ eur_checking:
|
||||||
balance: 12000
|
balance: 12000
|
||||||
accountable_type: Account::Depository
|
accountable_type: Account::Depository
|
||||||
accountable_id: "123e4567-e89b-12d3-a456-426614174004"
|
accountable_id: "123e4567-e89b-12d3-a456-426614174004"
|
||||||
|
institution: revolut
|
||||||
|
|
||||||
# Multi-currency account (e.g. Wise, Revolut, etc.)
|
# Multi-currency account (e.g. Wise, Revolut, etc.)
|
||||||
multi_currency:
|
multi_currency:
|
||||||
|
@ -46,3 +50,4 @@ multi_currency:
|
||||||
balance: 10000
|
balance: 10000
|
||||||
accountable_type: Account::Depository
|
accountable_type: Account::Depository
|
||||||
accountable_id: "123e4567-e89b-12d3-a456-426614174005"
|
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