mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 13:19:39 +02:00
Rework account views and addition flow (#1324)
* Move accountable partials * Split accountables into separate view partials * Fix test * Add form to permitted partials * Fix failing system tests * Update new account modal views * New sync algorithm implementation * Update account system test assertions to match new behavior * Fix off by 1 date error * Revert new balance sync algorithm * Add missing account overviews
This commit is contained in:
parent
c7c281073f
commit
e8e100e1d8
88 changed files with 763 additions and 526 deletions
|
@ -1,10 +0,0 @@
|
|||
class Account::LogosController < ApplicationController
|
||||
def show
|
||||
@account = Current.family.accounts.find(params[:account_id])
|
||||
render_placeholder
|
||||
end
|
||||
|
||||
def render_placeholder
|
||||
render formats: :svg
|
||||
end
|
||||
end
|
|
@ -23,11 +23,8 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
|
||||
def new
|
||||
@account = Account.new(
|
||||
accountable: Accountable.from_type(params[:type])&.new,
|
||||
currency: Current.family.currency
|
||||
)
|
||||
|
||||
@account = Account.new(currency: Current.family.currency)
|
||||
@account.accountable = Accountable.from_type(params[:type])&.new if params[:type].present?
|
||||
@account.accountable.address = Address.new if @account.accountable.is_a?(Property)
|
||||
|
||||
if params[:institution_id]
|
||||
|
@ -36,8 +33,6 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
|
||||
def show
|
||||
@series = @account.series(period: @period)
|
||||
@trend = @series.trend
|
||||
end
|
||||
|
||||
def edit
|
||||
|
@ -57,6 +52,7 @@ class AccountsController < ApplicationController
|
|||
start_date: account_params[:start_date],
|
||||
start_balance: account_params[:start_balance]
|
||||
@account.sync_later
|
||||
|
||||
redirect_back_or_to account_path(@account), notice: t(".success")
|
||||
end
|
||||
|
||||
|
|
|
@ -1,4 +1,14 @@
|
|||
module AccountsHelper
|
||||
def permitted_accountable_partial(account, name = nil)
|
||||
permitted_names = %w[tooltip header tabs form]
|
||||
folder = account.accountable_type.underscore
|
||||
name ||= account.accountable_type.underscore
|
||||
|
||||
raise "Unpermitted accountable partial: #{name}" unless permitted_names.include?(name)
|
||||
|
||||
"accounts/accountables/#{folder}/#{name}"
|
||||
end
|
||||
|
||||
def summary_card(title:, &block)
|
||||
content = capture(&block)
|
||||
render "accounts/summary_card", title: title, content: content
|
||||
|
|
|
@ -9,10 +9,6 @@ module ApplicationHelper
|
|||
content_for(:header_title) { page_title }
|
||||
end
|
||||
|
||||
def permitted_accountable_partial(name)
|
||||
name.underscore
|
||||
end
|
||||
|
||||
def family_notifications_stream
|
||||
turbo_stream_from [ Current.family, :notifications ] if Current.family
|
||||
end
|
||||
|
@ -80,8 +76,8 @@ module ApplicationHelper
|
|||
color = hex || "#1570EF" # blue-600
|
||||
|
||||
<<-STYLE.strip
|
||||
background-color: color-mix(in srgb, #{color} 5%, white);
|
||||
border-color: color-mix(in srgb, #{color} 10%, white);
|
||||
background-color: color-mix(in srgb, #{color} 10%, white);
|
||||
border-color: color-mix(in srgb, #{color} 30%, white);
|
||||
color: #{color};
|
||||
STYLE
|
||||
end
|
||||
|
|
|
@ -51,12 +51,17 @@ export default class extends Controller {
|
|||
_normalizeDataPoints() {
|
||||
this._normalDataPoints = (this.dataValue.values || []).map((d) => ({
|
||||
...d,
|
||||
date: new Date(d.date),
|
||||
date: this._parseDate(d.date),
|
||||
value: d.value.amount ? +d.value.amount : +d.value,
|
||||
currency: d.value.currency,
|
||||
}));
|
||||
}
|
||||
|
||||
_parseDate(dateString) {
|
||||
const [year, month, day] = dateString.split("-").map(Number);
|
||||
return new Date(year, month - 1, day);
|
||||
}
|
||||
|
||||
_rememberInitialContainerSize() {
|
||||
this._d3InitialContainerWidth = this._d3Container.node().clientWidth;
|
||||
this._d3InitialContainerHeight = this._d3Container.node().clientHeight;
|
||||
|
|
|
@ -27,6 +27,8 @@ class Account < ApplicationRecord
|
|||
scope :alphabetically, -> { order(:name) }
|
||||
scope :ungrouped, -> { where(institution_id: nil) }
|
||||
|
||||
has_one_attached :logo
|
||||
|
||||
delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy
|
||||
|
||||
accepts_nested_attributes_for :accountable
|
||||
|
|
57
app/models/account/balance/calculator.rb
Normal file
57
app/models/account/balance/calculator.rb
Normal file
|
@ -0,0 +1,57 @@
|
|||
class Account::Balance::Calculator
|
||||
def initialize(account, sync_start_date)
|
||||
@account = account
|
||||
@sync_start_date = sync_start_date
|
||||
end
|
||||
|
||||
def calculate(is_partial_sync: false)
|
||||
cached_entries = account.entries.where("date >= ?", sync_start_date).to_a
|
||||
sync_starting_balance = is_partial_sync ? find_start_balance_for_partial_sync : find_start_balance_for_full_sync(cached_entries)
|
||||
|
||||
prior_balance = sync_starting_balance
|
||||
|
||||
(sync_start_date..Date.current).map do |date|
|
||||
current_balance = calculate_balance_for_date(date, entries: cached_entries, prior_balance:)
|
||||
|
||||
prior_balance = current_balance
|
||||
|
||||
build_balance(date, current_balance)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
attr_reader :account, :sync_start_date
|
||||
|
||||
def find_start_balance_for_partial_sync
|
||||
account.balances.find_by(currency: account.currency, date: sync_start_date - 1.day).balance
|
||||
end
|
||||
|
||||
def find_start_balance_for_full_sync(cached_entries)
|
||||
account.balance + net_entry_flows(cached_entries)
|
||||
end
|
||||
|
||||
def calculate_balance_for_date(date, entries:, prior_balance:)
|
||||
valuation = entries.find { |e| e.date == date && e.account_valuation? }
|
||||
|
||||
return valuation.amount if valuation
|
||||
|
||||
entries = entries.select { |e| e.date == date }
|
||||
|
||||
prior_balance - net_entry_flows(entries)
|
||||
end
|
||||
|
||||
def net_entry_flows(entries, target_currency = account.currency)
|
||||
converted_entry_amounts = entries.map { |t| t.amount_money.exchange_to(target_currency, date: t.date) }
|
||||
|
||||
flows = converted_entry_amounts.sum(&:amount)
|
||||
|
||||
account.liability? ? flows * -1 : flows
|
||||
end
|
||||
|
||||
def build_balance(date, balance, currency = nil)
|
||||
account.balances.build \
|
||||
date: date,
|
||||
balance: balance,
|
||||
currency: currency || account.currency
|
||||
end
|
||||
end
|
46
app/models/account/balance/converter.rb
Normal file
46
app/models/account/balance/converter.rb
Normal file
|
@ -0,0 +1,46 @@
|
|||
class Account::Balance::Converter
|
||||
def initialize(account, sync_start_date)
|
||||
@account = account
|
||||
@sync_start_date = sync_start_date
|
||||
end
|
||||
|
||||
def convert(balances)
|
||||
calculate_converted_balances(balances)
|
||||
end
|
||||
|
||||
private
|
||||
attr_reader :account, :sync_start_date
|
||||
|
||||
def calculate_converted_balances(balances)
|
||||
from_currency = account.currency
|
||||
to_currency = account.family.currency
|
||||
|
||||
if ExchangeRate.exchange_rates_provider.nil?
|
||||
account.observe_missing_exchange_rate_provider
|
||||
return []
|
||||
end
|
||||
|
||||
exchange_rates = ExchangeRate.find_rates from: from_currency,
|
||||
to: to_currency,
|
||||
start_date: sync_start_date
|
||||
|
||||
missing_exchange_rates = balances.map(&:date) - exchange_rates.map(&:date)
|
||||
|
||||
if missing_exchange_rates.any?
|
||||
account.observe_missing_exchange_rates(from: from_currency, to: to_currency, dates: missing_exchange_rates)
|
||||
return []
|
||||
end
|
||||
|
||||
balances.map do |balance|
|
||||
exchange_rate = exchange_rates.find { |er| er.date == balance.date }
|
||||
build_balance(balance.date, exchange_rate.rate * balance.balance, to_currency)
|
||||
end
|
||||
end
|
||||
|
||||
def build_balance(date, balance, currency = nil)
|
||||
account.balances.build \
|
||||
date: date,
|
||||
balance: balance,
|
||||
currency: currency || account.currency
|
||||
end
|
||||
end
|
37
app/models/account/balance/loader.rb
Normal file
37
app/models/account/balance/loader.rb
Normal file
|
@ -0,0 +1,37 @@
|
|||
class Account::Balance::Loader
|
||||
def initialize(account)
|
||||
@account = account
|
||||
end
|
||||
|
||||
def load(balances, start_date)
|
||||
Account::Balance.transaction do
|
||||
upsert_balances!(balances)
|
||||
purge_stale_balances!(start_date)
|
||||
|
||||
account.reload
|
||||
|
||||
update_account_balance!(balances)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
attr_reader :account
|
||||
|
||||
def update_account_balance!(balances)
|
||||
last_balance = balances.select { |db| db.currency == account.currency }.last&.balance
|
||||
account.update! balance: last_balance if last_balance.present?
|
||||
end
|
||||
|
||||
def upsert_balances!(balances)
|
||||
current_time = Time.now
|
||||
balances_to_upsert = balances.map do |balance|
|
||||
balance.attributes.slice("date", "balance", "currency").merge("updated_at" => current_time)
|
||||
end
|
||||
|
||||
account.balances.upsert_all(balances_to_upsert, unique_by: %i[account_id date currency])
|
||||
end
|
||||
|
||||
def purge_stale_balances!(start_date)
|
||||
account.balances.delete_by("date < ?", start_date)
|
||||
end
|
||||
end
|
|
@ -1,133 +1,51 @@
|
|||
class Account::Balance::Syncer
|
||||
def initialize(account, start_date: nil)
|
||||
@account = account
|
||||
@provided_start_date = start_date
|
||||
@sync_start_date = calculate_sync_start_date(start_date)
|
||||
@loader = Account::Balance::Loader.new(account)
|
||||
@converter = Account::Balance::Converter.new(account, sync_start_date)
|
||||
@calculator = Account::Balance::Calculator.new(account, sync_start_date)
|
||||
end
|
||||
|
||||
def run
|
||||
daily_balances = calculate_daily_balances
|
||||
daily_balances += calculate_converted_balances(daily_balances) if account.currency != account.family.currency
|
||||
daily_balances = calculator.calculate(is_partial_sync: is_partial_sync?)
|
||||
daily_balances += converter.convert(daily_balances) if account.currency != account.family.currency
|
||||
|
||||
Account::Balance.transaction do
|
||||
upsert_balances!(daily_balances)
|
||||
purge_stale_balances!
|
||||
|
||||
if daily_balances.any?
|
||||
account.reload
|
||||
last_balance = daily_balances.select { |db| db.currency == account.currency }.last&.balance
|
||||
account.update! balance: last_balance
|
||||
end
|
||||
end
|
||||
loader.load(daily_balances, account_start_date)
|
||||
rescue Money::ConversionError => e
|
||||
account.observe_missing_exchange_rates(from: e.from_currency, to: e.to_currency, dates: [ e.date ])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :sync_start_date, :account
|
||||
|
||||
def upsert_balances!(balances)
|
||||
current_time = Time.now
|
||||
balances_to_upsert = balances.map do |balance|
|
||||
balance.attributes.slice("date", "balance", "currency").merge("updated_at" => current_time)
|
||||
end
|
||||
|
||||
account.balances.upsert_all(balances_to_upsert, unique_by: %i[account_id date currency])
|
||||
end
|
||||
|
||||
def purge_stale_balances!
|
||||
account.balances.delete_by("date < ?", account_start_date)
|
||||
end
|
||||
|
||||
def calculate_balance_for_date(date, entries:, prior_balance:)
|
||||
valuation = entries.find { |e| e.date == date && e.account_valuation? }
|
||||
|
||||
return valuation.amount if valuation
|
||||
return derived_sync_start_balance(entries) unless prior_balance
|
||||
|
||||
entries = entries.select { |e| e.date == date }
|
||||
|
||||
prior_balance - net_entry_flows(entries)
|
||||
end
|
||||
|
||||
def calculate_daily_balances
|
||||
entries = account.entries.where("date >= ?", sync_start_date).to_a
|
||||
prior_balance = find_prior_balance
|
||||
|
||||
(sync_start_date..Date.current).map do |date|
|
||||
current_balance = calculate_balance_for_date(date, entries:, prior_balance:)
|
||||
|
||||
prior_balance = current_balance
|
||||
|
||||
build_balance(date, current_balance)
|
||||
end
|
||||
end
|
||||
|
||||
def calculate_converted_balances(balances)
|
||||
from_currency = account.currency
|
||||
to_currency = account.family.currency
|
||||
|
||||
if ExchangeRate.exchange_rates_provider.nil?
|
||||
account.observe_missing_exchange_rate_provider
|
||||
return []
|
||||
end
|
||||
|
||||
exchange_rates = ExchangeRate.find_rates from: from_currency,
|
||||
to: to_currency,
|
||||
start_date: sync_start_date
|
||||
|
||||
missing_exchange_rates = balances.map(&:date) - exchange_rates.map(&:date)
|
||||
|
||||
if missing_exchange_rates.any?
|
||||
account.observe_missing_exchange_rates(from: from_currency, to: to_currency, dates: missing_exchange_rates)
|
||||
return []
|
||||
end
|
||||
|
||||
balances.map do |balance|
|
||||
exchange_rate = exchange_rates.find { |er| er.date == balance.date }
|
||||
build_balance(balance.date, exchange_rate.rate * balance.balance, to_currency)
|
||||
end
|
||||
end
|
||||
|
||||
def build_balance(date, balance, currency = nil)
|
||||
account.balances.build \
|
||||
date: date,
|
||||
balance: balance,
|
||||
currency: currency || account.currency
|
||||
end
|
||||
|
||||
def derived_sync_start_balance(entries)
|
||||
transactions_and_trades = entries.reject { |e| e.account_valuation? }.select { |e| e.date > sync_start_date }
|
||||
|
||||
account.balance + net_entry_flows(transactions_and_trades)
|
||||
end
|
||||
|
||||
def find_prior_balance
|
||||
account.balances.where(currency: account.currency).where("date < ?", sync_start_date).order(date: :desc).first&.balance
|
||||
end
|
||||
|
||||
def net_entry_flows(entries, target_currency = account.currency)
|
||||
converted_entry_amounts = entries.map { |t| t.amount_money.exchange_to(target_currency, date: t.date) }
|
||||
|
||||
flows = converted_entry_amounts.sum(&:amount)
|
||||
|
||||
account.liability? ? flows * -1 : flows
|
||||
end
|
||||
attr_reader :sync_start_date, :provided_start_date, :account, :loader, :converter, :calculator
|
||||
|
||||
def account_start_date
|
||||
@account_start_date ||= begin
|
||||
oldest_entry_date = account.entries.chronological.first.try(:date)
|
||||
oldest_entry = account.entries.chronological.first
|
||||
|
||||
return Date.current unless oldest_entry_date
|
||||
return Date.current unless oldest_entry.present?
|
||||
|
||||
oldest_entry_is_valuation = account.entries.account_valuations.where(date: oldest_entry_date).exists?
|
||||
|
||||
oldest_entry_date -= 1 unless oldest_entry_is_valuation
|
||||
oldest_entry_date
|
||||
if oldest_entry.account_valuation?
|
||||
oldest_entry.date
|
||||
else
|
||||
oldest_entry.date - 1.day
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def calculate_sync_start_date(provided_start_date)
|
||||
[ provided_start_date, account_start_date ].compact.max
|
||||
return provided_start_date if provided_start_date.present? && prior_balance_available?(provided_start_date)
|
||||
|
||||
account_start_date
|
||||
end
|
||||
|
||||
def prior_balance_available?(date)
|
||||
account.balances.find_by(currency: account.currency, date: date - 1.day).present?
|
||||
end
|
||||
|
||||
def is_partial_sync?
|
||||
sync_start_date == provided_start_date && sync_start_date < Date.current
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,4 +12,8 @@ class CreditCard < ApplicationRecord
|
|||
def annual_fee_money
|
||||
annual_fee ? Money.new(annual_fee, account.currency) : nil
|
||||
end
|
||||
|
||||
def color
|
||||
"#F13636"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
class Crypto < ApplicationRecord
|
||||
include Accountable
|
||||
|
||||
def color
|
||||
"#737373"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
class Depository < ApplicationRecord
|
||||
include Accountable
|
||||
|
||||
def color
|
||||
"#875BF7"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -46,4 +46,8 @@ class Investment < ApplicationRecord
|
|||
rescue Money::ConversionError
|
||||
TimeSeries.new([])
|
||||
end
|
||||
|
||||
def color
|
||||
"#1570EF"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,4 +16,8 @@ class Loan < ApplicationRecord
|
|||
|
||||
Money.new(payment.round, account.currency)
|
||||
end
|
||||
|
||||
def color
|
||||
"#D444F1"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
class OtherAsset < ApplicationRecord
|
||||
include Accountable
|
||||
|
||||
def color
|
||||
"#12B76A"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
class OtherLiability < ApplicationRecord
|
||||
include Accountable
|
||||
|
||||
def color
|
||||
"#737373"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,6 +19,10 @@ class Property < ApplicationRecord
|
|||
TimeSeries::Trend.new(current: account.balance_money, previous: first_valuation_amount)
|
||||
end
|
||||
|
||||
def color
|
||||
"#06AED4"
|
||||
end
|
||||
|
||||
private
|
||||
def first_valuation_amount
|
||||
account.entries.account_valuations.order(:date).first&.amount_money || account.balance_money
|
||||
|
|
|
@ -15,6 +15,10 @@ class Vehicle < ApplicationRecord
|
|||
TimeSeries::Trend.new(current: account.balance_money, previous: first_valuation_amount)
|
||||
end
|
||||
|
||||
def color
|
||||
"#F23E94"
|
||||
end
|
||||
|
||||
private
|
||||
def first_valuation_amount
|
||||
account.entries.account_valuations.order(:date).first&.amount_money || account.balance_money
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 32 32"
|
||||
aria-hidden="true">
|
||||
<g>
|
||||
<rect width="100%" height="100%" rx="50%" fill="<%= accountable_color(@account.accountable_type) %>" opacity="0.1" />
|
||||
<text
|
||||
x="50%"
|
||||
y="50%"
|
||||
fill="<%= accountable_color(@account.accountable_type) %>"
|
||||
text-anchor="middle"
|
||||
dy="0.35em"
|
||||
font-family="ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji"
|
||||
font-size="16"
|
||||
font-weight="400">
|
||||
<%= @account.name[0].upcase %>
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 653 B |
|
@ -30,7 +30,7 @@
|
|||
<% group.children.sort_by(&:name).each do |account_value_node| %>
|
||||
<% account = account_value_node.original %>
|
||||
<%= link_to account_path(account), class: "flex items-center w-full gap-3 px-3 py-2 mb-1 hover:bg-gray-100 rounded-[10px]" do %>
|
||||
<%= image_tag account_logo_url(account), class: "w-6 h-6" %>
|
||||
<%= render "accounts/logo", account: account, size: "sm" %>
|
||||
<div>
|
||||
<p class="font-medium"><%= account_value_node.name %></p>
|
||||
<% if account.subtype %>
|
||||
|
@ -63,7 +63,7 @@
|
|||
<% end %>
|
||||
<%= link_to new_account_path(step: "method", type: type.name.demodulize), class: "flex items-center min-h-10 gap-4 px-3 py-2 mb-1 text-gray-500 text-sm font-medium rounded-[10px] hover:bg-gray-100", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<p>New <%= type.model_name.human.downcase %></p>
|
||||
<%= t(".new_account", type: type.model_name.human.downcase) %>
|
||||
<% end %>
|
||||
</details>
|
||||
<% end %>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<%= link_to new_account_path(
|
||||
step: "method",
|
||||
type: type.class.name.demodulize,
|
||||
institution_id: params[:institution_id]
|
||||
),
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<%= 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 %>
|
||||
<%= link_to new_account_path(step: "method"), 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 %>
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
<% if local_assigns[:disabled] && disabled %>
|
||||
<span class="flex items-center w-full gap-4 p-2 px-2 text-center border border-transparent rounded-lg cursor-not-allowed focus:outline-none focus:bg-gray-50 focus:border focus:border-gray-200 hover:bg-gray-50">
|
||||
<%# locals: (text:, icon:, disabled: false) %>
|
||||
|
||||
<% if disabled %>
|
||||
<span class="flex items-center w-full gap-4 p-2 px-2 text-center border border-transparent rounded-lg cursor-not-allowed focus:outline-none focus:bg-gray-50 focus:border focus:border-gray-200 text-gray-400">
|
||||
<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>
|
||||
<%= text %>
|
||||
</span>
|
||||
<% else %>
|
||||
<%= 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 %>
|
||||
<%= link_to new_account_path(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>
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
<%= styled_form_with model: account, url: url, scope: :account, class: "flex flex-col gap-4 justify-between grow", data: { turbo: false } do |f| %>
|
||||
<div class="grow space-y-2">
|
||||
<%= f.hidden_field :accountable_type %>
|
||||
<%= f.text_field :name, placeholder: t(".name_placeholder"), required: "required", label: t(".name_label"), autofocus: true %>
|
||||
<%= f.select :accountable_type, Accountable::TYPES.map { |type| [type.titleize, type] }, { label: t(".accountable_type"), prompt: t(".type_prompt") }, required: true, autofocus: true %>
|
||||
<%= f.text_field :name, placeholder: t(".name_placeholder"), required: "required", label: t(".name_label") %>
|
||||
|
||||
<% if account.new_record? %>
|
||||
<%= f.hidden_field :institution_id %>
|
||||
|
@ -13,15 +13,10 @@
|
|||
|
||||
<%= f.money_field :balance, label: t(".balance"), required: true, default_currency: Current.family.currency %>
|
||||
|
||||
<% if account.new_record? %>
|
||||
<div class="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.yesterday, min: Account::Entry.min_supported_date %></div>
|
||||
<div class="w-1/2"><%= f.money_field :start_balance, label: t(".start_balance"), placeholder: 90, hide_currency: true, default_currency: Current.family.currency %></div>
|
||||
</div>
|
||||
<% if account.accountable %>
|
||||
<%= render permitted_accountable_partial(account, "form"), f: f %>
|
||||
<% end %>
|
||||
|
||||
<%= render "accounts/accountables/#{permitted_accountable_partial(account.accountable_type)}", f: f %>
|
||||
</div>
|
||||
|
||||
<%= f.submit "#{account.new_record? ? "Add" : "Update"} #{account.accountable.model_name.human.downcase}" %>
|
||||
<%= f.submit %>
|
||||
<% end %>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_account_path, class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2", data: { turbo_frame: :modal } do %>
|
||||
<%= link_to new_account_path(step: "method"), class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2", data: { turbo_frame: :modal } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<p class="text-sm font-medium"><%= t(".new") %></p>
|
||||
<% end %>
|
||||
|
|
14
app/views/accounts/_logo.html.erb
Normal file
14
app/views/accounts/_logo.html.erb
Normal file
|
@ -0,0 +1,14 @@
|
|||
<%# locals: (account:, size: "md") %>
|
||||
|
||||
<% size_classes = {
|
||||
"sm" => "w-6 h-6",
|
||||
"md" => "w-9 h-9",
|
||||
"lg" => "w-10 h-10",
|
||||
"full" => "w-full h-full"
|
||||
} %>
|
||||
|
||||
<% if account.logo.attached? %>
|
||||
<%= image_tag account.logo, class: size_classes[size] %>
|
||||
<% else %>
|
||||
<%= circle_logo(account.name, hex: account.accountable.color, size: size) %>
|
||||
<% end %>
|
33
app/views/accounts/_menu.html.erb
Normal file
33
app/views/accounts/_menu.html.erb
Normal file
|
@ -0,0 +1,33 @@
|
|||
<%# locals: (account:) %>
|
||||
|
||||
<%= 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,
|
||||
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" %>
|
||||
|
||||
<span><%= t(".import") %></span>
|
||||
<% end %>
|
||||
|
||||
<%= button_to account_path(account),
|
||||
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_html"),
|
||||
accept: t(".confirm_accept", name: account.name)
|
||||
}
|
||||
} do %>
|
||||
<%= lucide_icon("trash-2", class: "w-5 h-5 mr-2") %> Delete account
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
8
app/views/accounts/_new_account_setup_bar.html.erb
Normal file
8
app/views/accounts/_new_account_setup_bar.html.erb
Normal file
|
@ -0,0 +1,8 @@
|
|||
<div class="flex items-center gap-2 rounded-xl justify-between shadow-xs bg-white p-4 border border-alpha-black-25">
|
||||
<p class="text-lg font-medium text-gray-900">Setup your new account</p>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<%= link_to "Track balances only", new_account_valuation_path(@account), class: "btn btn--ghost", data: { turbo_frame: dom_id(@account.entries.account_valuations.new) } %>
|
||||
<%= link_to "Add your first transaction", new_transaction_path(account_id: @account.id), class: "btn btn--primary", data: { turbo_frame: :modal } %>
|
||||
</div>
|
||||
</div>
|
|
@ -1,4 +1,4 @@
|
|||
<%= button_to sync_all_accounts_path, class: "btn btn--outline flex items-center gap-2", title: "Sync All" do %>
|
||||
<%= button_to sync_all_accounts_path, class: "btn btn--outline flex items-center gap-2", title: t(".sync") do %>
|
||||
<%= lucide_icon "refresh-cw", class: "w-5 h-5" %>
|
||||
<span><%= t("accounts.sync_all.button_text") %></span>
|
||||
<span><%= t(".sync") %></span>
|
||||
<% end %>
|
||||
|
|
7
app/views/accounts/accountables/_default_header.html.erb
Normal file
7
app/views/accounts/accountables/_default_header.html.erb
Normal file
|
@ -0,0 +1,7 @@
|
|||
<div class="flex items-center gap-3">
|
||||
<%= render "accounts/logo", account: account %>
|
||||
|
||||
<div>
|
||||
<h2 class="font-medium text-xl"><%= account.name %></h2>
|
||||
</div>
|
||||
</div>
|
7
app/views/accounts/accountables/_default_tabs.html.erb
Normal file
7
app/views/accounts/accountables/_default_tabs.html.erb
Normal file
|
@ -0,0 +1,7 @@
|
|||
<%# locals: (account:) %>
|
||||
|
||||
<% if account.transactions.any? %>
|
||||
<%= render "accounts/accountables/transactions", account: account %>
|
||||
<% else %>
|
||||
<%= render "accounts/accountables/valuations", account: account %>
|
||||
<% end %>
|
8
app/views/accounts/accountables/_tab.html.erb
Normal file
8
app/views/accounts/accountables/_tab.html.erb
Normal file
|
@ -0,0 +1,8 @@
|
|||
<%# locals: (account:, key:, is_selected:) %>
|
||||
|
||||
<%= link_to key.titleize,
|
||||
account_path(account, tab: key),
|
||||
class: [
|
||||
"px-2 py-1.5 rounded-md border border-transparent",
|
||||
"bg-white shadow-xs border-alpha-black-50": is_selected
|
||||
] %>
|
5
app/views/accounts/accountables/_transactions.html.erb
Normal file
5
app/views/accounts/accountables/_transactions.html.erb
Normal file
|
@ -0,0 +1,5 @@
|
|||
<%# locals: (account:) %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(account, :transactions), src: account_transactions_path(account) do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% end %>
|
5
app/views/accounts/accountables/_valuations.html.erb
Normal file
5
app/views/accounts/accountables/_valuations.html.erb
Normal file
|
@ -0,0 +1,5 @@
|
|||
<%# locals: (account:) %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(account, :valuations), src: account_valuations_path(account) do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% end %>
|
|
@ -0,0 +1 @@
|
|||
<%= render "accounts/accountables/default_header", account: account %>
|
|
@ -25,3 +25,7 @@
|
|||
<%= format_money(account.credit_card.annual_fee_money || Money.new(0, account.currency)) %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center py-8">
|
||||
<%= link_to "Edit account details", edit_account_path(account), class: "btn btn--ghost", data: { turbo_frame: :modal } %>
|
||||
</div>
|
15
app/views/accounts/accountables/credit_card/_tabs.html.erb
Normal file
15
app/views/accounts/accountables/credit_card/_tabs.html.erb
Normal file
|
@ -0,0 +1,15 @@
|
|||
<%# locals: (account:, selected_tab:) %>
|
||||
|
||||
<div class="flex gap-2 text-sm text-gray-900 font-medium mb-4">
|
||||
<%= render "accounts/accountables/tab", account: account, key: "overview", is_selected: selected_tab.in?([nil, "overview"]) %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "transactions", is_selected: selected_tab == "transactions" %>
|
||||
</div>
|
||||
|
||||
<div class="min-h-[800px]">
|
||||
<% case selected_tab %>
|
||||
<% when nil, "overview" %>
|
||||
<%= render "accounts/accountables/credit_card/overview", account: account %>
|
||||
<% when "transactions" %>
|
||||
<%= render "accounts/accountables/transactions", account: account %>
|
||||
<% end %>
|
||||
</div>
|
1
app/views/accounts/accountables/crypto/_header.html.erb
Normal file
1
app/views/accounts/accountables/crypto/_header.html.erb
Normal file
|
@ -0,0 +1 @@
|
|||
<%= render "accounts/accountables/default_header", account: account %>
|
1
app/views/accounts/accountables/crypto/_tabs.html.erb
Normal file
1
app/views/accounts/accountables/crypto/_tabs.html.erb
Normal file
|
@ -0,0 +1 @@
|
|||
<%= render "accounts/accountables/default_tabs", account: account %>
|
|
@ -0,0 +1 @@
|
|||
<%= render "accounts/accountables/default_header", account: account %>
|
|
@ -0,0 +1 @@
|
|||
<%= render "accounts/accountables/default_tabs", account: account %>
|
|
@ -0,0 +1 @@
|
|||
<%= render "accounts/accountables/default_header", account: account %>
|
28
app/views/accounts/accountables/investment/_tabs.html.erb
Normal file
28
app/views/accounts/accountables/investment/_tabs.html.erb
Normal file
|
@ -0,0 +1,28 @@
|
|||
<%# locals: (account:, selected_tab:) %>
|
||||
|
||||
<% if account.entries.account_trades.any? || account.entries.account_transactions.any? %>
|
||||
<div class="flex gap-2 text-sm text-gray-900 font-medium mb-4">
|
||||
<%= render "accounts/accountables/tab", account: account, key: "holdings", is_selected: selected_tab.in?([nil, "holdings"]) %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "cash", is_selected: selected_tab == "cash" %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "transactions", is_selected: selected_tab == "transactions" %>
|
||||
</div>
|
||||
|
||||
<div class="min-h-[800px]">
|
||||
<% case selected_tab %>
|
||||
<% when nil, "holdings" %>
|
||||
<%= turbo_frame_tag dom_id(account, :holdings), src: account_holdings_path(account) do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% end %>
|
||||
<% when "cash" %>
|
||||
<%= turbo_frame_tag dom_id(account, :cash), src: account_cashes_path(account) do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% end %>
|
||||
<% when "transactions" %>
|
||||
<%= turbo_frame_tag dom_id(account, :trades), src: account_trades_path(account) do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= render "accounts/accountables/valuations", account: account %>
|
||||
<% end %>
|
|
@ -1,4 +1,5 @@
|
|||
<%# locals: (account:) -%>
|
||||
|
||||
<div data-controller="tooltip" data-tooltip-placement-value="right" data-tooltip-offset-value=10 data-tooltip-cross-axis-value=50>
|
||||
<%= lucide_icon("info", class: "w-4 h-4 shrink-0 text-gray-500") %>
|
||||
<div role="tooltip" data-tooltip-target="tooltip" class="tooltip bg-gray-700 text-sm p-2 rounded w-64">
|
1
app/views/accounts/accountables/loan/_header.html.erb
Normal file
1
app/views/accounts/accountables/loan/_header.html.erb
Normal file
|
@ -0,0 +1 @@
|
|||
<%= render "accounts/accountables/default_header", account: account %>
|
|
@ -43,3 +43,7 @@
|
|||
<%= account.loan.rate_type&.titleize || t(".unknown") %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center py-8">
|
||||
<%= link_to "Edit account details", edit_account_path(account), class: "btn btn--ghost", data: { turbo_frame: :modal } %>
|
||||
</div>
|
15
app/views/accounts/accountables/loan/_tabs.html.erb
Normal file
15
app/views/accounts/accountables/loan/_tabs.html.erb
Normal file
|
@ -0,0 +1,15 @@
|
|||
<%# locals: (account:, selected_tab:) %>
|
||||
|
||||
<div class="flex gap-2 text-sm text-gray-900 font-medium mb-4">
|
||||
<%= render "accounts/accountables/tab", account: account, key: "overview", is_selected: selected_tab.in?([nil, "overview"]) %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "value", is_selected: selected_tab == "value" %>
|
||||
</div>
|
||||
|
||||
<div class="min-h-[800px]">
|
||||
<% case selected_tab %>
|
||||
<% when nil, "overview" %>
|
||||
<%= render "accounts/accountables/loan/overview", account: account %>
|
||||
<% when "value" %>
|
||||
<%= render "accounts/accountables/valuations", account: account %>
|
||||
<% end %>
|
||||
</div>
|
|
@ -0,0 +1 @@
|
|||
<%= render "accounts/accountables/default_header", account: account %>
|
|
@ -0,0 +1 @@
|
|||
<%= render "accounts/accountables/valuations", account: account %>
|
|
@ -0,0 +1 @@
|
|||
<%= render "accounts/accountables/default_header", account: account %>
|
|
@ -0,0 +1 @@
|
|||
<%= render "accounts/accountables/valuations", account: account %>
|
11
app/views/accounts/accountables/property/_header.html.erb
Normal file
11
app/views/accounts/accountables/property/_header.html.erb
Normal file
|
@ -0,0 +1,11 @@
|
|||
<div class="flex items-center gap-3">
|
||||
<%= render "accounts/logo", account: account %>
|
||||
|
||||
<div>
|
||||
<h2 class="font-medium text-xl"><%= account.name %></h2>
|
||||
|
||||
<% if account.property.address&.line1.present? %>
|
||||
<p class="text-gray-500"><%= account.property.address %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
|
@ -27,3 +27,7 @@
|
|||
<%= account.property.area || t(".unknown") %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center py-8">
|
||||
<%= link_to "Edit account details", edit_account_path(account), class: "btn btn--ghost", data: { turbo_frame: :modal } %>
|
||||
</div>
|
||||
|
|
15
app/views/accounts/accountables/property/_tabs.html.erb
Normal file
15
app/views/accounts/accountables/property/_tabs.html.erb
Normal file
|
@ -0,0 +1,15 @@
|
|||
<%# locals: (account:, selected_tab:) %>
|
||||
|
||||
<div class="flex gap-2 text-sm text-gray-900 font-medium mb-4">
|
||||
<%= render "accounts/accountables/tab", account: account, key: "overview", is_selected: selected_tab.in?([nil, "overview"]) %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "value", is_selected: selected_tab == "value" %>
|
||||
</div>
|
||||
|
||||
<div class="min-h-[800px]">
|
||||
<% case selected_tab %>
|
||||
<% when nil, "overview" %>
|
||||
<%= render "accounts/accountables/property/overview", account: account %>
|
||||
<% when "value" %>
|
||||
<%= render "accounts/accountables/valuations", account: account %>
|
||||
<% end %>
|
||||
</div>
|
1
app/views/accounts/accountables/vehicle/_header.html.erb
Normal file
1
app/views/accounts/accountables/vehicle/_header.html.erb
Normal file
|
@ -0,0 +1 @@
|
|||
<%= render "accounts/accountables/default_header", account: account %>
|
|
@ -31,3 +31,7 @@
|
|||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center py-8">
|
||||
<%= link_to "Edit account details", edit_account_path(account), class: "btn btn--ghost", data: { turbo_frame: :modal } %>
|
||||
</div>
|
||||
|
|
15
app/views/accounts/accountables/vehicle/_tabs.html.erb
Normal file
15
app/views/accounts/accountables/vehicle/_tabs.html.erb
Normal file
|
@ -0,0 +1,15 @@
|
|||
<%# locals: (account:, selected_tab:) %>
|
||||
|
||||
<div class="flex gap-2 text-sm text-gray-900 font-medium mb-4">
|
||||
<%= render "accounts/accountables/tab", account: account, key: "overview", is_selected: selected_tab.in?([nil, "overview"]) %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "value", is_selected: selected_tab == "value" %>
|
||||
</div>
|
||||
|
||||
<div class="min-h-[800px]">
|
||||
<% case selected_tab %>
|
||||
<% when nil, "overview" %>
|
||||
<%= render "accounts/accountables/vehicle/overview", account: account %>
|
||||
<% when "value" %>
|
||||
<%= render "accounts/accountables/valuations", account: account %>
|
||||
<% end %>
|
||||
</div>
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
<%= render "sync_all_button" %>
|
||||
|
||||
<%= link_to new_account_path,
|
||||
<%= link_to new_account_path(step: "method"),
|
||||
data: { turbo_frame: "modal" },
|
||||
class: "btn btn--primary flex items-center gap-1" do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
|
|
|
@ -1,58 +1,24 @@
|
|||
<h1 class="text-3xl font-semibold font-display"><%= t(".title") %></h1>
|
||||
<%= modal do %>
|
||||
<div class="flex flex-col w-screen max-w-xl" data-controller="list-keyboard-navigation">
|
||||
<% if @account.accountable.blank? %>
|
||||
<div class="border-b border-alpha-black-25 p-4 text-gray-400">
|
||||
<%= t ".select_accountable_type" %>
|
||||
</div>
|
||||
<div class="flex flex-col p-2 text-sm grow">
|
||||
<button hidden data-controller="hotkey" data-hotkey="k,K,ArrowUp,ArrowLeft" data-action="list-keyboard-navigation#focusPrevious">Previous</button>
|
||||
<button hidden data-controller="hotkey" data-hotkey="j,J,ArrowDown,ArrowRight" data-action="list-keyboard-navigation#focusNext">Next</button>
|
||||
<%= render "account_type", type: Depository.new, bg_color: "bg-blue-500/5", text_color: "text-blue-500", icon: "landmark" %>
|
||||
<%= render "account_type", type: Investment.new, bg_color: "bg-green-500/5", text_color: "text-green-500", icon: "line-chart" %>
|
||||
<%= render "account_type", type: Crypto.new, bg_color: "bg-orange-500/5", text_color: "text-orange-500", icon: "bitcoin" %>
|
||||
<%= render "account_type", type: Property.new, bg_color: "bg-pink-500/5", text_color: "text-pink-500", icon: "home" %>
|
||||
<%= render "account_type", type: Vehicle.new, bg_color: "bg-cyan-500/5", text_color: "text-cyan-500", icon: "car-front" %>
|
||||
<%= render "account_type", type: CreditCard.new, bg_color: "bg-violet-500/5", text_color: "text-violet-500", icon: "credit-card" %>
|
||||
<%= render "account_type", type: Loan.new, bg_color: "bg-yellow-500/5", text_color: "text-yellow-500", icon: "hand-coins" %>
|
||||
<%= render "account_type", type: OtherAsset.new, bg_color: "bg-green-500/5", text_color: "text-green-500", icon: "plus" %>
|
||||
<%= render "account_type", type: OtherLiability.new, bg_color: "bg-red-500/5", text_color: "text-red-500", icon: "minus" %>
|
||||
</div>
|
||||
<div class="border-t border-alpha-black-25 p-4 text-gray-500 text-sm flex justify-between">
|
||||
<div class="flex space-x-5">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span>Select</span>
|
||||
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon("corner-down-left", class: "inline w-3 h-3") %></kbd>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span>Navigate</span>
|
||||
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon("arrow-up", class: "inline w-3 h-3") %></kbd>
|
||||
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon("arrow-down", class: "inline w-3 h-3") %></kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button data-action="modal#close">Close</button>
|
||||
<kbd class="bg-alpha-black-50 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-8 h-5 shrink-0 grow-0 items-center justify-center text-xs">ESC</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<% elsif params[:step] == 'method' && @account.accountable.present? %>
|
||||
<% if params[:step] == 'method' %>
|
||||
<div class="border-b border-alpha-black-25 p-4 text-gray-400 flex items-center space-x-3">
|
||||
<%= link_to new_account_path, class: "flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-alpha-black-50 back focus:outline-gray-300 focus:outline" do %>
|
||||
<%= lucide_icon("arrow-left", class: "text-gray-500 w-5 h-5") %>
|
||||
<% end %>
|
||||
<span>How would you like to add it?</span>
|
||||
</div>
|
||||
<div class="flex flex-col p-2 text-sm grow">
|
||||
<button hidden data-controller="hotkey" data-hotkey="k,K,ArrowUp,ArrowLeft" data-action="list-keyboard-navigation#focusPrevious">Previous</button>
|
||||
<button hidden data-controller="hotkey" data-hotkey="j,J,ArrowDown,ArrowRight" data-action="list-keyboard-navigation#focusNext">Next</button>
|
||||
<%= render "entry_method", type: @account.accountable, text: "Enter account balance manually", icon: "keyboard" %>
|
||||
<%= link_to new_import_path, 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 %>
|
||||
|
||||
<%= render "entry_method", text: t(".manual_entry"), icon: "keyboard" %>
|
||||
|
||||
<%= link_to new_import_path(import: { type: "AccountImport" }), 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("sheet", class: "text-gray-500 w-5 h-5") %>
|
||||
</span>
|
||||
Upload CSV
|
||||
<%= t(".csv_entry") %>
|
||||
<% end %>
|
||||
<%= render "entry_method", type: @account.accountable, text: "Securely link bank account with data provider (coming soon)", icon: "link-2", disabled: true %>
|
||||
|
||||
<%= render "entry_method", text: t(".connected_entry"), icon: "link-2", disabled: true %>
|
||||
</div>
|
||||
<div class="border-t border-alpha-black-25 p-4 text-gray-500 text-sm flex justify-between">
|
||||
<div class="flex space-x-5">
|
||||
|
@ -73,13 +39,13 @@
|
|||
</div>
|
||||
<% else %>
|
||||
<div class="border-b border-alpha-black-25 p-4 text-gray-800 flex items-center space-x-3">
|
||||
<%= link_to new_account_path(step: "method", type: params[:type]), class: "flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-alpha-black-50 focus:outline-gray-300 focus:outline" do %>
|
||||
<%= link_to new_account_path(step: "method"), class: "flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-alpha-black-50 focus:outline-gray-300 focus:outline" do %>
|
||||
<%= lucide_icon("arrow-left", class: "text-gray-500 w-5 h-5") %>
|
||||
<% end %>
|
||||
<span>Add <%= @account.accountable.model_name.human.downcase %></span>
|
||||
<span>Add account</span>
|
||||
</div>
|
||||
|
||||
<div class="p-4 pt-1">
|
||||
<div class="p-4">
|
||||
<%= render "form", account: @account, url: new_account_form_url(@account) %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -1,56 +1,25 @@
|
|||
<%= turbo_stream_from @account %>
|
||||
|
||||
<%= tag.div id: dom_id(@account), class: "space-y-4" do %>
|
||||
<header class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
<%= image_tag account_logo_url(@account), class: "w-8 h-8" %>
|
||||
<div>
|
||||
<h2 class="font-medium text-xl"><%= @account.name %></h2>
|
||||
<% series = @account.series(period: @period) %>
|
||||
<% trend = series.trend %>
|
||||
|
||||
<% if @account.property? && @account.property.address&.line1.present? %>
|
||||
<p class="text-gray-500"><%= @account.property.address %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<%= tag.div id: dom_id(@account), class: "space-y-4" do %>
|
||||
<header class="flex items-center gap-4">
|
||||
<%= render permitted_accountable_partial(@account, "header"), account: @account %>
|
||||
|
||||
<div class="flex items-center gap-3 ml-auto">
|
||||
<%= 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-500 hover:text-gray-400" %>
|
||||
<% end %>
|
||||
|
||||
<%= 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,
|
||||
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" %>
|
||||
|
||||
<span><%= t(".import") %></span>
|
||||
<% end %>
|
||||
|
||||
<%= button_to account_path(@account),
|
||||
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_html"),
|
||||
accept: t(".confirm_accept", name: @account.name)
|
||||
}
|
||||
} do %>
|
||||
<%= lucide_icon("trash-2", class: "w-5 h-5 mr-2") %> Delete account
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= render "menu", account: @account %>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<% if @account.entries.empty? && @account.depository? %>
|
||||
<%= render "accounts/new_account_setup_bar", account: @account %>
|
||||
<% end %>
|
||||
|
||||
<% if @account.highest_priority_issue %>
|
||||
<%= render partial: "issues/issue", locals: { issue: @account.highest_priority_issue } %>
|
||||
<% end %>
|
||||
|
@ -66,47 +35,35 @@
|
|||
<%= tag.p t(".total_owed"), class: "text-sm font-medium text-gray-500" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= render "tooltip", account: @account if @account.investment? %>
|
||||
|
||||
<%= render permitted_accountable_partial(@account, "tooltip"), account: @account if @account.investment? %>
|
||||
</div>
|
||||
|
||||
<%= tag.p format_money(@account.value), class: "text-gray-900 text-3xl font-medium" %>
|
||||
|
||||
<div>
|
||||
<% if @series.trend.direction.flat? %>
|
||||
<% if trend.direction.flat? %>
|
||||
<%= tag.span t(".no_change"), class: "text-gray-500" %>
|
||||
<% else %>
|
||||
<%= tag.span format_money(@series.trend.value), style: "color: #{@trend.color}" %>
|
||||
<%= tag.span "(#{@trend.percent}%)", style: "color: #{@trend.color}" %>
|
||||
<%= tag.span format_money(trend.value), style: "color: #{trend.color}" %>
|
||||
<%= tag.span "(#{trend.percent}%)", style: "color: #{trend.color}" %>
|
||||
<% end %>
|
||||
|
||||
<%= tag.span period_label(@period), class: "text-gray-500" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= form_with url: account_path(@account), method: :get, data: { controller: "auto-submit-form" } do |form| %>
|
||||
<%= period_select form: form, selected: @period.name %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="h-96 flex items-center justify-center text-2xl font-bold">
|
||||
<%= render partial: "shared/line_chart", locals: { series: @series } %>
|
||||
<%= render "shared/line_chart", series: @account.series(period: @period) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% selected_tab = selected_account_tab(@account) %>
|
||||
<% selected_tab_key = selected_tab[:key] %>
|
||||
<% selected_tab_partial_path = selected_tab[:partial_path] %>
|
||||
<% selected_tab_route = selected_tab[:route] %>
|
||||
|
||||
<div class="flex gap-2 text-sm text-gray-900 font-medium mb-4">
|
||||
<% account_tabs(@account).each do |tab| %>
|
||||
<%= link_to tab[:label], tab[:path], class: ["px-2 py-1.5 rounded-md border border-transparent", "bg-white shadow-xs border-alpha-black-50": selected_tab_key == tab[:key]] %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="min-h-[800px]">
|
||||
<% if selected_tab_route.present? %>
|
||||
<%= turbo_frame_tag dom_id(@account, selected_tab_key), src: selected_tab_route do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= render selected_tab_partial_path, account: @account %>
|
||||
<% end %>
|
||||
<%= render permitted_accountable_partial(@account, "tabs"), account: @account, selected_tab: params[:tab] %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
<div class="flex justify-between items-center mb-5">
|
||||
<h2 class="text-lg font-medium text-gray-900">Assets</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= link_to new_account_path, class: "btn btn--secondary flex items-center gap-1", data: { turbo_frame: "modal" } do %>
|
||||
<%= link_to new_account_path(step: "method"), class: "btn btn--secondary flex items-center gap-1", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %>
|
||||
<p><%= t(".new") %></p>
|
||||
<% end %>
|
||||
|
@ -66,7 +66,7 @@
|
|||
<div class="flex justify-between items-center mb-5">
|
||||
<h2 class="text-lg font-medium text-gray-900">Liabilities</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= link_to new_account_path, class: "btn btn--secondary flex items-center gap-1", data: { turbo_frame: "modal" } do %>
|
||||
<%= link_to new_account_path(step: "method"), class: "btn btn--secondary flex items-center gap-1", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %>
|
||||
<p><%= t(".new") %></p>
|
||||
<% end %>
|
||||
|
|
|
@ -15,14 +15,14 @@
|
|||
<h3 class="uppercase text-gray-500 text-xs font-medium px-3 py-1.5"><%= t(".sources") %></h3>
|
||||
<ul class="bg-white border border-alpha-black-25 rounded-lg shadow-xs">
|
||||
<li>
|
||||
<% if @pending_import.present? %>
|
||||
<% if @pending_import.present? && (params[:type].nil? || params[:type] == @pending_import.type) %>
|
||||
<%= link_to import_path(@pending_import), class: "flex items-center justify-between p-4 group cursor-pointer", data: { turbo: false } do %>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="bg-orange-500/5 rounded-md w-8 h-8 flex items-center justify-center">
|
||||
<%= lucide_icon("loader", class: "w-5 h-5 text-orange-500") %>
|
||||
</div>
|
||||
<span class="text-sm text-gray-900 group-hover:text-gray-700">
|
||||
<%= t(".resume") %>
|
||||
<%= t(".resume", type: @pending_import.type.titleize) %>
|
||||
</span>
|
||||
</div>
|
||||
<%= lucide_icon("chevron-right", class: "w-5 h-5 text-gray-500") %>
|
||||
|
@ -33,6 +33,8 @@
|
|||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
<% if Current.family.accounts.any? && (params[:type].nil? || params[:type] == "TransactionImport") %>
|
||||
<li>
|
||||
<%= button_to imports_path(import: { type: "TransactionImport" }), class: "flex items-center justify-between p-4 group cursor-pointer w-full", data: { turbo: false } do %>
|
||||
<div class="flex items-center gap-2">
|
||||
|
@ -50,7 +52,9 @@
|
|||
<div class="h-px bg-alpha-black-50"></div>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
<% if Current.family.accounts.any? && (params[:type].nil? || params[:type] == "TradeImport") %>
|
||||
<li>
|
||||
<%= button_to imports_path(import: { type: "TradeImport" }), class: "flex items-center justify-between p-4 group cursor-pointer w-full", data: { turbo: false } do %>
|
||||
<div class="flex items-center gap-2">
|
||||
|
@ -68,7 +72,9 @@
|
|||
<div class="h-px bg-alpha-black-50"></div>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
<% if params[:type].nil? || params[:type] == "AccountImport" %>
|
||||
<li>
|
||||
<%= button_to imports_path(import: { type: "AccountImport" }), class: "flex items-center justify-between p-4 group cursor-pointer w-full", data: { turbo: false } do %>
|
||||
<div class="flex items-center gap-2">
|
||||
|
@ -86,7 +92,9 @@
|
|||
<div class="h-px bg-alpha-black-50"></div>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
<% if Current.family.accounts.any? && (params[:type].nil? || params[:type] == "MintImport" || params[:type] == "TransactionImport") %>
|
||||
<li>
|
||||
<%= button_to imports_path(import: { type: "MintImport" }), class: "flex items-center justify-between p-4 group w-full", data: { turbo: false } do %>
|
||||
<div class="flex items-center gap-2">
|
||||
|
@ -102,6 +110,7 @@
|
|||
<div class="h-px bg-alpha-black-50"></div>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -103,7 +103,7 @@
|
|||
<%= period_select form: form, selected: "last_30_days", classes: "w-full border-none pl-2 pr-7 text-xs bg-transparent gap-1 cursor-pointer font-semibold tracking-wide focus:outline-none focus:ring-0" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= link_to new_account_path, id: "sidebar-new-account", class: "block hover:bg-gray-100 font-semibold text-gray-900 flex items-center rounded", title: t(".new_account"), data: { turbo_frame: "modal" } do %>
|
||||
<%= link_to new_account_path(step: "method"), id: "sidebar-new-account", class: "block hover:bg-gray-100 font-semibold text-gray-900 flex items-center rounded p-1", title: t(".new_account"), data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
@ -114,7 +114,7 @@
|
|||
<%= render "accounts/account_list", group: group %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= link_to new_account_path, class: "flex items-center min-h-10 gap-4 px-3 py-2 mb-1 text-gray-500 text-sm font-medium rounded-[10px] hover:bg-gray-100", data: { turbo_frame: "modal" } do %>
|
||||
<%= link_to new_account_path(step: "method"), class: "flex items-center min-h-10 gap-4 px-3 py-2 mb-1 text-gray-500 text-sm font-medium rounded-[10px] hover:bg-gray-100", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<%= tag.p t(".new_account") %>
|
||||
<% end %>
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<% accountable_group.children.map do |account_value_node| %>
|
||||
<div class="flex items-center justify-between text-sm font-medium text-gray-900">
|
||||
<div class="flex items-center gap-4">
|
||||
<%= image_tag account_logo_url(account_value_node.original), class: "w-8 h-8" %>
|
||||
<%= render "accounts/logo", account: account_value_node.original, size: "sm" %>
|
||||
<div>
|
||||
<p><%= account_value_node.name %></p>
|
||||
</div>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_account_path, class: "flex items-center gap-1 btn btn--primary", data: { turbo_frame: "modal" } do %>
|
||||
<%= link_to new_account_path(step: "method"), class: "flex items-center gap-1 btn btn--primary", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<span><%= t(".new") %></span>
|
||||
<% end %>
|
||||
|
@ -70,10 +70,10 @@
|
|||
data-time-series-chart-use-labels-value="false"
|
||||
data-time-series-chart-use-tooltip-value="false"></div>
|
||||
</div>
|
||||
<div class="flex gap-1.5">
|
||||
<div class="flex gap-1.5 mt-auto">
|
||||
<% @top_earners.first(3).each do |account| %>
|
||||
<%= link_to account, class: "border border-alpha-black-25 rounded-full p-1 pr-2 flex items-center gap-1 text-xs text-gray-900 font-medium hover:bg-gray-25", data: { controller: "tooltip" } do %>
|
||||
<%= image_tag account_logo_url(account), class: "w-5 h-5" %>
|
||||
<%= render "accounts/logo", account: account, size: "sm" %>
|
||||
<span>+<%= Money.new(account.income, account.currency) %></span>
|
||||
<%= render partial: "shared/text_tooltip", locals: { tooltip_text: account.name } %>
|
||||
<% end %>
|
||||
|
@ -103,10 +103,10 @@
|
|||
data-time-series-chart-use-labels-value="false"
|
||||
data-time-series-chart-use-tooltip-value="false"></div>
|
||||
</div>
|
||||
<div class="flex gap-1.5">
|
||||
<div class="mt-auto flex gap-1.5">
|
||||
<% @top_spenders.first(3).each do |account| %>
|
||||
<%= link_to account, class: "border border-alpha-black-25 rounded-full p-1 pr-2 flex items-center gap-1 text-xs text-gray-900 font-medium hover:bg-gray-25", data: { controller: "tooltip" } do %>
|
||||
<%= image_tag account_logo_url(account), class: "w-5 h-5" %>
|
||||
<%= render "accounts/logo", account: account, size: "sm" %>
|
||||
-<%= Money.new(account.spending, account.currency) %>
|
||||
<%= render partial: "shared/text_tooltip", locals: { tooltip_text: account.name } %>
|
||||
<% end %>
|
||||
|
@ -141,7 +141,7 @@
|
|||
<% @top_savers.first(3).each do |account| %>
|
||||
<% unless account.savings_rate.infinite? %>
|
||||
<%= link_to account, class: "border border-alpha-black-25 rounded-full p-1 pr-2 flex items-center gap-1 text-xs text-gray-900 font-medium hover:bg-gray-25", data: { controller: "tooltip" } do %>
|
||||
<%= image_tag account_logo_url(account), class: "w-5 h-5" %>
|
||||
<%= render "accounts/logo", account: account, size: "sm" %>
|
||||
<span><%= account.savings_rate > 0 ? "+" : "-" %><%= number_to_percentage(account.savings_rate.abs * 100, precision: 2) %></span>
|
||||
<%= render partial: "shared/text_tooltip", locals: { tooltip_text: account.name } %>
|
||||
<% end %>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<% size_classes = {
|
||||
"sm" => "w-6 h-6",
|
||||
"md" => "w-8 h-8",
|
||||
"md" => "w-9 h-9",
|
||||
"lg" => "w-10 h-10",
|
||||
"full" => "w-full h-full"
|
||||
} %>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<p class="text-gray-500"><%= t(".no_account_subtitle") %></p>
|
||||
</div>
|
||||
|
||||
<%= link_to new_account_path, class: "btn btn--primary flex items-center gap-1", data: { turbo_frame: "modal" } do %>
|
||||
<%= link_to new_account_path(step: "method"), class: "btn btn--primary flex items-center gap-1", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<span><%= t(".new_account") %></span>
|
||||
<% end %>
|
||||
|
|
|
@ -23,6 +23,74 @@
|
|||
],
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"warning_type": "Dynamic Render Path",
|
||||
"warning_code": 15,
|
||||
"fingerprint": "42595161ffdc9ce9a10c4ba2a75fd2bb668e273bc4e683880b0ea906d0bd28f8",
|
||||
"check_name": "Render",
|
||||
"message": "Render path contains parameter value",
|
||||
"file": "app/views/accounts/show.html.erb",
|
||||
"line": 8,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
||||
"code": "render(action => permitted_accountable_partial(Current.family.accounts.find(params[:id]), \"header\"), { :account => Current.family.accounts.find(params[:id]) })",
|
||||
"render_path": [
|
||||
{
|
||||
"type": "controller",
|
||||
"class": "AccountsController",
|
||||
"method": "show",
|
||||
"line": 39,
|
||||
"file": "app/controllers/accounts_controller.rb",
|
||||
"rendered": {
|
||||
"name": "accounts/show",
|
||||
"file": "app/views/accounts/show.html.erb"
|
||||
}
|
||||
}
|
||||
],
|
||||
"location": {
|
||||
"type": "template",
|
||||
"template": "accounts/show"
|
||||
},
|
||||
"user_input": "params[:id]",
|
||||
"confidence": "Weak",
|
||||
"cwe_id": [
|
||||
22
|
||||
],
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"warning_type": "Dynamic Render Path",
|
||||
"warning_code": 15,
|
||||
"fingerprint": "a35b18785608dbdf35607501363573576ed8c304039f8387997acd1408ca1025",
|
||||
"check_name": "Render",
|
||||
"message": "Render path contains parameter value",
|
||||
"file": "app/views/accounts/show.html.erb",
|
||||
"line": 35,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
||||
"code": "render(action => permitted_accountable_partial(Current.family.accounts.find(params[:id]), \"tooltip\"), { :account => Current.family.accounts.find(params[:id]) })",
|
||||
"render_path": [
|
||||
{
|
||||
"type": "controller",
|
||||
"class": "AccountsController",
|
||||
"method": "show",
|
||||
"line": 39,
|
||||
"file": "app/controllers/accounts_controller.rb",
|
||||
"rendered": {
|
||||
"name": "accounts/show",
|
||||
"file": "app/views/accounts/show.html.erb"
|
||||
}
|
||||
}
|
||||
],
|
||||
"location": {
|
||||
"type": "template",
|
||||
"template": "accounts/show"
|
||||
},
|
||||
"user_input": "params[:id]",
|
||||
"confidence": "Weak",
|
||||
"cwe_id": [
|
||||
22
|
||||
],
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"warning_type": "Cross-Site Scripting",
|
||||
"warning_code": 2,
|
||||
|
@ -38,7 +106,7 @@
|
|||
"type": "controller",
|
||||
"class": "PagesController",
|
||||
"method": "changelog",
|
||||
"line": 35,
|
||||
"line": 36,
|
||||
"file": "app/controllers/pages_controller.rb",
|
||||
"rendered": {
|
||||
"name": "pages/changelog",
|
||||
|
@ -60,19 +128,19 @@
|
|||
{
|
||||
"warning_type": "Dynamic Render Path",
|
||||
"warning_code": 15,
|
||||
"fingerprint": "b7a59d6dd91f4d30873b271659636c7975e25b47f436b4f03900a08809af2e92",
|
||||
"fingerprint": "c5c512a13c34c9696024bd4e2367a657a5c140b5b6a0f5c352e9b69965f63e1b",
|
||||
"check_name": "Render",
|
||||
"message": "Render path contains parameter value",
|
||||
"file": "app/views/accounts/show.html.erb",
|
||||
"line": 105,
|
||||
"line": 63,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
||||
"code": "render(action => selected_account_tab(Current.family.accounts.find(params[:id]))[:partial_path], { :account => Current.family.accounts.find(params[:id]) })",
|
||||
"code": "render(action => permitted_accountable_partial(Current.family.accounts.find(params[:id]), \"tabs\"), { :account => Current.family.accounts.find(params[:id]), :selected_tab => params[:tab] })",
|
||||
"render_path": [
|
||||
{
|
||||
"type": "controller",
|
||||
"class": "AccountsController",
|
||||
"method": "show",
|
||||
"line": 38,
|
||||
"line": 39,
|
||||
"file": "app/controllers/accounts_controller.rb",
|
||||
"rendered": {
|
||||
"name": "accounts/show",
|
||||
|
@ -98,7 +166,7 @@
|
|||
"check_name": "Render",
|
||||
"message": "Render path contains parameter value",
|
||||
"file": "app/views/import/configurations/show.html.erb",
|
||||
"line": 13,
|
||||
"line": 15,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
||||
"code": "render(partial => permitted_import_configuration_path(Current.family.imports.find(params[:import_id])), { :locals => ({ :import => Current.family.imports.find(params[:import_id]) }) })",
|
||||
"render_path": [
|
||||
|
@ -126,6 +194,6 @@
|
|||
"note": ""
|
||||
}
|
||||
],
|
||||
"updated": "2024-09-28 13:27:09 -0400",
|
||||
"updated": "2024-10-17 11:30:15 -0400",
|
||||
"brakeman_version": "6.2.1"
|
||||
}
|
||||
|
|
|
@ -1,37 +1,88 @@
|
|||
---
|
||||
en:
|
||||
accounts:
|
||||
sync_all_button:
|
||||
sync: Sync all
|
||||
account:
|
||||
has_issues: Issue detected.
|
||||
troubleshoot: Troubleshoot
|
||||
account_list:
|
||||
new_account: "New %{type}"
|
||||
empty:
|
||||
no_accounts: No accounts yet
|
||||
empty_message: Add an account either via connection, importing or entering manually.
|
||||
new_account: New account
|
||||
form:
|
||||
name_label: Account name
|
||||
name_placeholder: Example account name
|
||||
institution: Financial institution
|
||||
ungrouped: "(none)"
|
||||
balance: Today's balance
|
||||
accountable_type: Account type
|
||||
type_prompt: Select a type
|
||||
header:
|
||||
accounts: Accounts
|
||||
manage: Manage accounts
|
||||
new: New account
|
||||
institution_accounts:
|
||||
add_account_to_institution: Add new account
|
||||
has_issues: Issue detected, see accounts
|
||||
syncing: Syncing...
|
||||
status: "Last synced %{last_synced_at} ago"
|
||||
status_never: Requires data sync
|
||||
edit: Edit institution
|
||||
delete: Delete institution
|
||||
confirm_title: Delete financial 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_accept: Delete institution
|
||||
new_account: Add account
|
||||
institutionless_accounts:
|
||||
other_accounts: Other accounts
|
||||
menu:
|
||||
edit: Edit
|
||||
import: Import transactions
|
||||
confirm_title: Delete account?
|
||||
confirm_body_html: "<p>By deleting this account, you will erase its value history, affecting various aspects of your overall account. This action will have a direct impact on your net worth calculations and the account graphs.</p><br /> <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_accept: 'Delete "%{name}"'
|
||||
accountables:
|
||||
investment:
|
||||
prompt: Select a subtype
|
||||
none: None
|
||||
credit_card:
|
||||
annual_fee: Annual fee
|
||||
annual_fee_placeholder: '99'
|
||||
apr: APR
|
||||
apr_placeholder: '15.99'
|
||||
form:
|
||||
available_credit: Available credit
|
||||
available_credit_placeholder: '10000'
|
||||
expiration_date: Expiration date
|
||||
minimum_payment: Minimum payment
|
||||
minimum_payment_placeholder: '100'
|
||||
apr: APR
|
||||
apr_placeholder: '15.99'
|
||||
expiration_date: Expiration date
|
||||
annual_fee: Annual fee
|
||||
annual_fee_placeholder: '99'
|
||||
overview:
|
||||
amount_owed: Amount Owed
|
||||
annual_fee: Annual Fee
|
||||
apr: APR
|
||||
available_credit: Available Credit
|
||||
expiration_date: Expiration Date
|
||||
minimum_payment: Minimum Payment
|
||||
apr: APR
|
||||
expiration_date: Expiration Date
|
||||
annual_fee: Annual Fee
|
||||
unknown: Unknown
|
||||
depository:
|
||||
prompt: Select a subtype
|
||||
form:
|
||||
none: None
|
||||
prompt: Select a subtype
|
||||
investment:
|
||||
form:
|
||||
none: None
|
||||
prompt: Select a subtype
|
||||
tooltip:
|
||||
cash: Cash
|
||||
holdings: Holdings
|
||||
total_value_tooltip: The total value is the sum of cash balance and your holdings value, minus margin loans.
|
||||
loan:
|
||||
form:
|
||||
interest_rate: Interest rate
|
||||
interest_rate_placeholder: '5.25'
|
||||
rate_type: Rate type
|
||||
term_months: Term (months)
|
||||
term_months_placeholder: '360'
|
||||
overview:
|
||||
interest_rate: Interest Rate
|
||||
monthly_payment: Monthly Payment
|
||||
|
@ -41,10 +92,8 @@ en:
|
|||
term: Term
|
||||
type: Type
|
||||
unknown: Unknown
|
||||
rate_type: Rate type
|
||||
term_months: Term (months)
|
||||
term_months_placeholder: '360'
|
||||
property:
|
||||
form:
|
||||
additional_info: Additional info
|
||||
area_unit: Area unit
|
||||
area_value: Area value
|
||||
|
@ -53,6 +102,9 @@ en:
|
|||
line1: Address line 1
|
||||
line2: Address line 2
|
||||
optional: optional
|
||||
postal_code: Postal code
|
||||
state: State
|
||||
year_built: Year built
|
||||
overview:
|
||||
living_area: Living Area
|
||||
market_value: Market Value
|
||||
|
@ -60,10 +112,8 @@ en:
|
|||
trend: Trend
|
||||
unknown: Unknown
|
||||
year_built: Year Built
|
||||
postal_code: Postal code
|
||||
state: State
|
||||
year_built: Year built
|
||||
vehicle:
|
||||
form:
|
||||
make: Make
|
||||
make_placeholder: Toyota
|
||||
mileage: Mileage
|
||||
|
@ -71,6 +121,8 @@ en:
|
|||
mileage_unit: Unit
|
||||
model: Model
|
||||
model_placeholder: Camry
|
||||
year: Year
|
||||
year_placeholder: '2023'
|
||||
overview:
|
||||
current_price: Current Price
|
||||
make_model: Make & Model
|
||||
|
@ -79,70 +131,22 @@ en:
|
|||
trend: Trend
|
||||
unknown: Unknown
|
||||
year: Year
|
||||
year: Year
|
||||
year_placeholder: '2023'
|
||||
create:
|
||||
success: New account created successfully
|
||||
destroy:
|
||||
success: Account deleted successfully
|
||||
edit:
|
||||
edit: Edit %{account}
|
||||
empty:
|
||||
empty_message: Add an account either via connection, importing or entering manually.
|
||||
new_account: New account
|
||||
no_accounts: No accounts yet
|
||||
form:
|
||||
institution: Financial institution
|
||||
ungrouped: "(none)"
|
||||
balance: Current balance
|
||||
name_label: Account name
|
||||
name_placeholder: Example account name
|
||||
start_balance: Start balance (optional)
|
||||
start_date: Start date (optional)
|
||||
header:
|
||||
accounts: Accounts
|
||||
manage: Manage accounts
|
||||
new: New account
|
||||
edit: "Edit %{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
|
||||
has_issues: Issue detected, see accounts
|
||||
new_account: Add account
|
||||
status: Last synced %{last_synced_at} ago
|
||||
status_never: Requires data sync
|
||||
syncing: Syncing...
|
||||
institutionless_accounts:
|
||||
other_accounts: Other accounts
|
||||
new:
|
||||
select_accountable_type: What would you like to add?
|
||||
title: Add an account
|
||||
manual_entry: Enter account manually
|
||||
csv_entry: Import accounts CSV
|
||||
connected_entry: Securely link account with Plaid (coming soon)
|
||||
show:
|
||||
cash: Cash
|
||||
confirm_accept: Delete "%{name}"
|
||||
confirm_body_html: "<p>By deleting this account, you will erase its value history,
|
||||
affecting various aspects of your overall account. This action will have a
|
||||
direct impact on your net worth calculations and the account graphs.</p><br
|
||||
/> <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
|
||||
holdings: Holdings
|
||||
import: Import transactions
|
||||
no_change: No change
|
||||
overview: Overview
|
||||
sync_message_missing_rates: Since exchange rates haven't been synced, balance
|
||||
graphs may not reflect accurate values.
|
||||
sync_message_unknown_error: An error has occurred during the sync.
|
||||
total_owed: Total Owed
|
||||
total_value: Total Value
|
||||
trades: Transactions
|
||||
|
@ -151,21 +155,17 @@ en:
|
|||
summary:
|
||||
new: New
|
||||
no_assets: No assets found
|
||||
no_assets_description: Add an asset either via connection, importing or entering
|
||||
manually.
|
||||
no_assets_description: Add an asset either via connection, importing or entering manually.
|
||||
no_liabilities: No liabilities found
|
||||
no_liabilities_description: Add a liability either via connection, importing
|
||||
or entering manually.
|
||||
sync_all:
|
||||
button_text: Sync all
|
||||
success: Successfully queued accounts for syncing.
|
||||
tooltip:
|
||||
cash: Cash
|
||||
holdings: Holdings
|
||||
total_value_tooltip: The total value is the sum of cash balance and your holdings
|
||||
value, minus margin loans.
|
||||
no_liabilities_description: Add a liability either via connection, importing or entering manually.
|
||||
create:
|
||||
success: New account created successfully
|
||||
destroy:
|
||||
success: Account deleted successfully
|
||||
update:
|
||||
success: Account updated
|
||||
sync_all:
|
||||
success: Successfully queued accounts for syncing.
|
||||
credit_cards:
|
||||
create:
|
||||
success: Credit card created successfully
|
||||
|
|
|
@ -69,7 +69,7 @@ en:
|
|||
import_mint: Import from Mint
|
||||
import_portfolio: Import investments
|
||||
import_transactions: Import transactions
|
||||
resume: Resume latest import
|
||||
resume: Resume %{type}
|
||||
sources: Sources
|
||||
title: New CSV Import
|
||||
ready:
|
||||
|
|
|
@ -61,8 +61,6 @@ Rails.application.routes.draw do
|
|||
end
|
||||
|
||||
scope module: :account do
|
||||
resource :logo, only: :show
|
||||
|
||||
resources :holdings, only: %i[index new show destroy]
|
||||
resources :cashes, only: :index
|
||||
|
||||
|
|
|
@ -11,12 +11,6 @@ class ApplicationHelperTest < ActionView::TestCase
|
|||
assert_equal "Test Header Title", content_for(:header_title)
|
||||
end
|
||||
|
||||
test "#permitted_accountable_partial(accountable_type)" do
|
||||
assert_equal "account", permitted_accountable_partial("Account")
|
||||
assert_equal "user", permitted_accountable_partial("User")
|
||||
assert_equal "admin_user", permitted_accountable_partial("AdminUser")
|
||||
end
|
||||
|
||||
def setup
|
||||
@account1 = Account.new(currency: "USD", balance: 1)
|
||||
@account2 = Account.new(currency: "USD", balance: 2)
|
||||
|
|
|
@ -35,15 +35,6 @@ class Account::Balance::SyncerTest < ActiveSupport::TestCase
|
|||
assert_equal [ 19600, 19500, 19500, 20000, 20000, 20000 ], @account.balances.chronological.map(&:balance)
|
||||
end
|
||||
|
||||
test "syncs account with trades only" do
|
||||
aapl = securities(:aapl)
|
||||
create_trade(aapl, account: @investment_account, date: 1.day.ago.to_date, qty: 10)
|
||||
|
||||
run_sync_for @investment_account
|
||||
|
||||
assert_equal [ 52140, 50000, 50000 ], @investment_account.balances.chronological.map(&:balance)
|
||||
end
|
||||
|
||||
test "syncs account with valuations and transactions" do
|
||||
create_valuation(account: @account, date: 5.days.ago.to_date, amount: 20000)
|
||||
create_transaction(account: @account, date: 3.days.ago.to_date, amount: -500)
|
||||
|
|
|
@ -80,19 +80,15 @@ class AccountsTest < ApplicationSystemTestCase
|
|||
end
|
||||
|
||||
def assert_account_created(accountable_type, &block)
|
||||
click_link humanized_accountable(accountable_type)
|
||||
click_link "Enter account balance manually"
|
||||
click_link "Enter account manually"
|
||||
|
||||
account_name = "[system test] #{accountable_type} Account"
|
||||
|
||||
select accountable_type.titleize, from: "Account type"
|
||||
fill_in "Account name", with: account_name
|
||||
fill_in "account[balance]", with: 100.99
|
||||
fill_in "Start date (optional)", with: 10.days.ago.to_date
|
||||
fill_in "account[start_balance]", with: 95.25
|
||||
|
||||
yield if block_given?
|
||||
|
||||
click_button "Add #{humanized_accountable(accountable_type).downcase}"
|
||||
click_button "Create Account"
|
||||
|
||||
find("details", text: humanized_accountable(accountable_type)).click
|
||||
assert_text account_name
|
||||
|
@ -107,8 +103,10 @@ class AccountsTest < ApplicationSystemTestCase
|
|||
click_on "Edit"
|
||||
end
|
||||
|
||||
yield if block_given?
|
||||
|
||||
fill_in "Account name", with: "Updated account name"
|
||||
click_button "Update #{humanized_accountable(accountable_type).downcase}"
|
||||
click_button "Update Account"
|
||||
assert_selector "h2", text: "Updated account name"
|
||||
end
|
||||
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
require "application_system_test_case"
|
||||
|
||||
class TooltipsTest < ApplicationSystemTestCase
|
||||
include ActionView::Helpers::NumberHelper
|
||||
include ApplicationHelper
|
||||
|
||||
setup do
|
||||
sign_in @user = users(:family_admin)
|
||||
@account = accounts(:investment)
|
||||
end
|
||||
|
||||
test "can see account information tooltip" do
|
||||
visit account_path(@account)
|
||||
tooltip_element = find('[data-controller="tooltip"]')
|
||||
tooltip_element.hover
|
||||
tooltip_contents = find('[data-tooltip-target="tooltip"]')
|
||||
assert tooltip_contents.visible?
|
||||
within tooltip_contents do
|
||||
assert_text I18n.t("accounts.tooltip.total_value_tooltip")
|
||||
assert_text I18n.t("accounts.tooltip.holdings")
|
||||
assert_text format_money(@account.investment.holdings_value, precision: 0)
|
||||
assert_text I18n.t("accounts.tooltip.cash")
|
||||
assert_text format_money(@account.balance_money, precision: 0)
|
||||
end
|
||||
find("body").click
|
||||
assert find('[data-tooltip-target="tooltip"]', visible: false)
|
||||
end
|
||||
end
|
|
@ -62,6 +62,6 @@ class TradesTest < ApplicationSystemTestCase
|
|||
end
|
||||
|
||||
def visit_account_trades
|
||||
visit account_url(@account, tab: "trades")
|
||||
visit account_url(@account, tab: "transactions")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -156,6 +156,7 @@ class TransactionsTest < ApplicationSystemTestCase
|
|||
|
||||
test "can create deposit transaction for investment account" do
|
||||
investment_account = accounts(:investment)
|
||||
investment_account.entries.create!(name: "Investment account", date: Date.current, amount: 1000, currency: "USD", entryable: Account::Transaction.new)
|
||||
transfer_date = Date.current
|
||||
visit account_path(investment_account)
|
||||
click_on "New transaction"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue