mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 05:09:38 +02:00
Account Activity View + Account Forms (#1406)
* Remove balance mode, sketch out refactor * Activity view checkpoint * Entry partials, checkpoint * Finish txn partial * Give entries context when editing for different turbo responses * Calculate change of balance for each entry * Account tabs consolidation * Translations, linting, brakeman updates * Account actions concern * Finalize forms, get account system tests passing * Get tests passing * Lint, rubocop, schema updates * Improve routing and stream responses * Fix broken routes * Add import option for adding accounts * Fix system test * Fix test specificity * Fix sparklines * Improve account redirects
This commit is contained in:
parent
12e4f1067d
commit
65db49273c
216 changed files with 2043 additions and 1620 deletions
|
@ -4,13 +4,21 @@ class Account::EntriesController < ApplicationController
|
|||
before_action :set_account
|
||||
before_action :set_entry, only: %i[edit update show destroy]
|
||||
|
||||
def index
|
||||
@q = search_params
|
||||
@pagy, @entries = pagy(@account.entries.search(@q).reverse_chronological, limit: params[:per_page] || "10")
|
||||
end
|
||||
|
||||
def edit
|
||||
render entryable_view_path(:edit)
|
||||
end
|
||||
|
||||
def update
|
||||
prev_amount = @entry.amount
|
||||
prev_date = @entry.date
|
||||
|
||||
@entry.update!(entry_params)
|
||||
@entry.sync_account_later
|
||||
@entry.sync_account_later if prev_amount != @entry.amount || prev_date != @entry.date
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to account_entry_path(@account, @entry), notice: t(".success") }
|
||||
|
@ -43,6 +51,11 @@ class Account::EntriesController < ApplicationController
|
|||
end
|
||||
|
||||
def entry_params
|
||||
params.require(:account_entry).permit(:name, :date, :amount, :currency)
|
||||
params.require(:account_entry).permit(:name, :date, :amount, :currency, :notes)
|
||||
end
|
||||
|
||||
def search_params
|
||||
params.fetch(:q, {})
|
||||
.permit(:search)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,10 +17,10 @@ class Account::TradesController < ApplicationController
|
|||
|
||||
if entry = @builder.save
|
||||
entry.sync_account_later
|
||||
redirect_to account_path(@account), notice: t(".success")
|
||||
redirect_to @account, notice: t(".success")
|
||||
else
|
||||
flash[:alert] = t(".failure")
|
||||
redirect_back_or_to account_path(@account)
|
||||
redirect_back_or_to @account
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -12,16 +12,25 @@ class Account::TransactionsController < ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
@entry.update!(entry_params)
|
||||
prev_amount = @entry.amount
|
||||
prev_date = @entry.date
|
||||
|
||||
@entry.update!(entry_params.except(:origin))
|
||||
@entry.sync_account_later if prev_amount != @entry.amount || prev_date != @entry.date
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to account_entry_path(@account, @entry), notice: t(".success") }
|
||||
format.turbo_stream { render turbo_stream: turbo_stream.replace(@entry) }
|
||||
format.turbo_stream do
|
||||
render turbo_stream: turbo_stream.replace(
|
||||
@entry,
|
||||
partial: "account/entries/entry",
|
||||
locals: entry_locals.merge(entry: @entry)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:account_id])
|
||||
end
|
||||
|
@ -30,10 +39,18 @@ class Account::TransactionsController < ApplicationController
|
|||
@entry = @account.entries.find(params[:id])
|
||||
end
|
||||
|
||||
def entry_locals
|
||||
{
|
||||
selectable: entry_params[:origin].present?,
|
||||
show_balance: entry_params[:origin] == "account",
|
||||
origin: entry_params[:origin]
|
||||
}
|
||||
end
|
||||
|
||||
def entry_params
|
||||
params.require(:account_entry)
|
||||
.permit(
|
||||
:name, :date, :amount, :currency, :excluded, :notes, :entryable_type, :nature,
|
||||
:name, :date, :amount, :currency, :excluded, :notes, :entryable_type, :nature, :origin,
|
||||
entryable_attributes: [
|
||||
:id,
|
||||
:category_id,
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
class Account::TransfersController < ApplicationController
|
||||
layout :with_sidebar
|
||||
|
||||
before_action :set_transfer, only: :destroy
|
||||
before_action :set_transfer, only: %i[destroy show update]
|
||||
|
||||
def new
|
||||
@transfer = Account::Transfer.new
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def create
|
||||
from_account = Current.family.accounts.find(transfer_params[:from_account_id])
|
||||
to_account = Current.family.accounts.find(transfer_params[:to_account_id])
|
||||
|
@ -27,18 +30,33 @@ class Account::TransfersController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@transfer.update_entries!(transfer_update_params)
|
||||
redirect_back_or_to transactions_url, notice: t(".success")
|
||||
end
|
||||
|
||||
def destroy
|
||||
@transfer.destroy_and_remove_marks!
|
||||
@transfer.destroy!
|
||||
redirect_back_or_to transactions_url, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_transfer
|
||||
@transfer = Account::Transfer.find(params[:id])
|
||||
record = Account::Transfer.find(params[:id])
|
||||
|
||||
unless record.entries.all? { |entry| Current.family.accounts.include?(entry.account) }
|
||||
raise ActiveRecord::RecordNotFound
|
||||
end
|
||||
|
||||
@transfer = record
|
||||
end
|
||||
|
||||
def transfer_params
|
||||
params.require(:account_transfer).permit(:from_account_id, :to_account_id, :amount, :date, :name)
|
||||
params.require(:account_transfer).permit(:from_account_id, :to_account_id, :amount, :date, :name, :excluded)
|
||||
end
|
||||
|
||||
def transfer_update_params
|
||||
params.require(:account_transfer).permit(:excluded, :notes)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,7 +15,7 @@ class Account::ValuationsController < ApplicationController
|
|||
redirect_back_or_to account_valuations_path(@account), notice: t(".success")
|
||||
else
|
||||
flash[:alert] = @entry.errors.full_messages.to_sentence
|
||||
redirect_to account_path(@account)
|
||||
redirect_to @account
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
class AccountsController < ApplicationController
|
||||
layout :with_sidebar
|
||||
|
||||
include Filterable
|
||||
before_action :set_account, only: %i[edit show destroy sync update]
|
||||
before_action :set_account, only: %i[sync]
|
||||
|
||||
def index
|
||||
@institutions = Current.family.institutions
|
||||
|
@ -10,6 +9,7 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
|
||||
def summary
|
||||
@period = Period.from_param(params[:period])
|
||||
snapshot = Current.family.snapshot(@period)
|
||||
@net_worth_series = snapshot[:net_worth_series]
|
||||
@asset_series = snapshot[:asset_series]
|
||||
|
@ -22,45 +22,6 @@ class AccountsController < ApplicationController
|
|||
render layout: false
|
||||
end
|
||||
|
||||
def new
|
||||
@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]
|
||||
@account.institution = Current.family.institutions.find_by(id: params[:institution_id])
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def edit
|
||||
@account.accountable.build_address if @account.accountable.is_a?(Property) && @account.accountable.address.blank?
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update_with_sync!(account_params)
|
||||
redirect_back_or_to account_path(@account), notice: t(".success")
|
||||
end
|
||||
|
||||
def create
|
||||
@account = Current.family
|
||||
.accounts
|
||||
.create_with_optional_start_balance! \
|
||||
attributes: account_params.except(:start_date, :start_balance),
|
||||
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
|
||||
|
||||
def destroy
|
||||
@account.destroy!
|
||||
redirect_to accounts_path, notice: t(".success")
|
||||
end
|
||||
|
||||
def sync
|
||||
unless @account.syncing?
|
||||
@account.sync_later
|
||||
|
@ -73,12 +34,7 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:id])
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.require(:account).permit(:name, :accountable_type, :mode, :balance, :start_date, :start_balance, :currency, :subtype, :is_active, :institution_id)
|
||||
end
|
||||
end
|
||||
|
|
60
app/controllers/concerns/accountable_resource.rb
Normal file
60
app/controllers/concerns/accountable_resource.rb
Normal file
|
@ -0,0 +1,60 @@
|
|||
module AccountableResource
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
layout :with_sidebar
|
||||
before_action :set_account, only: [ :show, :edit, :update, :destroy ]
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def permitted_accountable_attributes(*attrs)
|
||||
@permitted_accountable_attributes = attrs if attrs.any?
|
||||
@permitted_accountable_attributes ||= [ :id ]
|
||||
end
|
||||
end
|
||||
|
||||
def new
|
||||
@account = Current.family.accounts.build(
|
||||
currency: Current.family.currency,
|
||||
accountable: accountable_type.new,
|
||||
institution_id: params[:institution_id]
|
||||
)
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def create
|
||||
@account = Current.family.accounts.create_and_sync(account_params.except(:return_to))
|
||||
redirect_to account_params[:return_to].presence || @account, notice: t(".success")
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update_with_sync!(account_params.except(:return_to))
|
||||
redirect_back_or_to @account, notice: t(".success")
|
||||
end
|
||||
|
||||
def destroy
|
||||
@account.destroy!
|
||||
redirect_to accounts_path, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
def accountable_type
|
||||
controller_name.classify.constantize
|
||||
end
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:id])
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.require(:account).permit(
|
||||
:name, :is_active, :balance, :subtype, :currency, :institution_id, :accountable_type, :return_to,
|
||||
accountable_attributes: self.class.permitted_accountable_attributes
|
||||
)
|
||||
end
|
||||
end
|
|
@ -1,23 +0,0 @@
|
|||
module Filterable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :set_period
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_period
|
||||
@period = Period.find_by_name(params[:period])
|
||||
if @period.nil?
|
||||
start_date = params[:start_date].presence&.to_date
|
||||
end_date = params[:end_date].presence&.to_date
|
||||
if start_date.is_a?(Date) && end_date.is_a?(Date) && start_date <= end_date
|
||||
@period = Period.new(name: "custom", date_range: start_date..end_date)
|
||||
else
|
||||
params[:period] = "last_30_days"
|
||||
@period = Period.find_by_name(params[:period])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,41 +1,12 @@
|
|||
class CreditCardsController < ApplicationController
|
||||
before_action :set_account, only: :update
|
||||
include AccountableResource
|
||||
|
||||
def create
|
||||
account = Current.family
|
||||
.accounts
|
||||
.create_with_optional_start_balance! \
|
||||
attributes: account_params.except(:start_date, :start_balance),
|
||||
start_date: account_params[:start_date],
|
||||
start_balance: account_params[:start_balance]
|
||||
|
||||
account.sync_later
|
||||
redirect_to account, notice: t(".success")
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update_with_sync!(account_params)
|
||||
redirect_to @account, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:id])
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.require(:account)
|
||||
.permit(
|
||||
:name, :balance, :institution_id, :mode, :start_date, :start_balance, :currency, :accountable_type,
|
||||
accountable_attributes: [
|
||||
:id,
|
||||
:available_credit,
|
||||
:minimum_payment,
|
||||
:apr,
|
||||
:annual_fee,
|
||||
:expiration_date
|
||||
]
|
||||
)
|
||||
end
|
||||
permitted_accountable_attributes(
|
||||
:id,
|
||||
:available_credit,
|
||||
:minimum_payment,
|
||||
:apr,
|
||||
:annual_fee,
|
||||
:expiration_date
|
||||
)
|
||||
end
|
||||
|
|
3
app/controllers/cryptos_controller.rb
Normal file
3
app/controllers/cryptos_controller.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
class CryptosController < ApplicationController
|
||||
include AccountableResource
|
||||
end
|
3
app/controllers/depositories_controller.rb
Normal file
3
app/controllers/depositories_controller.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
class DepositoriesController < ApplicationController
|
||||
include AccountableResource
|
||||
end
|
3
app/controllers/investments_controller.rb
Normal file
3
app/controllers/investments_controller.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
class InvestmentsController < ApplicationController
|
||||
include AccountableResource
|
||||
end
|
|
@ -3,8 +3,9 @@ class Issue::ExchangeRateProviderMissingsController < ApplicationController
|
|||
|
||||
def update
|
||||
Setting.synth_api_key = exchange_rate_params[:synth_api_key]
|
||||
@issue.issuable.sync_later
|
||||
redirect_back_or_to account_path(@issue.issuable)
|
||||
account = @issue.issuable
|
||||
account.sync_later
|
||||
redirect_back_or_to account
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -1,39 +1,7 @@
|
|||
class LoansController < ApplicationController
|
||||
before_action :set_account, only: :update
|
||||
include AccountableResource
|
||||
|
||||
def create
|
||||
account = Current.family
|
||||
.accounts
|
||||
.create_with_optional_start_balance! \
|
||||
attributes: account_params.except(:start_date, :start_balance),
|
||||
start_date: account_params[:start_date],
|
||||
start_balance: account_params[:start_balance]
|
||||
|
||||
account.sync_later
|
||||
redirect_to account, notice: t(".success")
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update_with_sync!(account_params)
|
||||
redirect_to @account, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:id])
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.require(:account)
|
||||
.permit(
|
||||
:name, :balance, :institution_id, :start_date, :mode, :start_balance, :currency, :accountable_type,
|
||||
accountable_attributes: [
|
||||
:id,
|
||||
:rate_type,
|
||||
:interest_rate,
|
||||
:term_months
|
||||
]
|
||||
)
|
||||
end
|
||||
permitted_accountable_attributes(
|
||||
:id, :rate_type, :interest_rate, :term_months
|
||||
)
|
||||
end
|
||||
|
|
3
app/controllers/other_assets_controller.rb
Normal file
3
app/controllers/other_assets_controller.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
class OtherAssetsController < ApplicationController
|
||||
include AccountableResource
|
||||
end
|
3
app/controllers/other_liabilities_controller.rb
Normal file
3
app/controllers/other_liabilities_controller.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
class OtherLiabilitiesController < ApplicationController
|
||||
include AccountableResource
|
||||
end
|
|
@ -2,9 +2,8 @@ class PagesController < ApplicationController
|
|||
skip_before_action :authenticate_user!, only: %i[early_access]
|
||||
layout :with_sidebar, except: %i[early_access]
|
||||
|
||||
include Filterable
|
||||
|
||||
def dashboard
|
||||
@period = Period.from_param(params[:period])
|
||||
snapshot = Current.family.snapshot(@period)
|
||||
@net_worth_series = snapshot[:net_worth_series]
|
||||
@asset_series = snapshot[:asset_series]
|
||||
|
|
|
@ -1,40 +1,22 @@
|
|||
class PropertiesController < ApplicationController
|
||||
before_action :set_account, only: :update
|
||||
include AccountableResource
|
||||
|
||||
def create
|
||||
account = Current.family
|
||||
.accounts
|
||||
.create_with_optional_start_balance! \
|
||||
attributes: account_params.except(:start_date, :start_balance),
|
||||
start_date: account_params[:start_date],
|
||||
start_balance: account_params[:start_balance]
|
||||
permitted_accountable_attributes(
|
||||
:id, :year_built, :area_unit, :area_value,
|
||||
address_attributes: [ :line1, :line2, :locality, :region, :country, :postal_code ]
|
||||
)
|
||||
|
||||
account.sync_later
|
||||
redirect_to account, notice: t(".success")
|
||||
def new
|
||||
@account = Current.family.accounts.build(
|
||||
currency: Current.family.currency,
|
||||
accountable: Property.new(
|
||||
address: Address.new
|
||||
),
|
||||
institution_id: params[:institution_id]
|
||||
)
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update_with_sync!(account_params)
|
||||
redirect_to @account, notice: t(".success")
|
||||
def edit
|
||||
@account.accountable.address ||= Address.new
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:id])
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.require(:account)
|
||||
.permit(
|
||||
:name, :balance, :institution_id, :start_date, :mode, :start_balance, :currency, :accountable_type,
|
||||
accountable_attributes: [
|
||||
:id,
|
||||
:year_built,
|
||||
:area_unit,
|
||||
:area_value,
|
||||
address_attributes: [ :line1, :line2, :locality, :region, :country, :postal_code ]
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -32,7 +32,7 @@ class TransactionsController < ApplicationController
|
|||
.create!(transaction_entry_params.merge(amount: amount))
|
||||
|
||||
@entry.sync_account_later
|
||||
redirect_back_or_to account_path(@entry.account), notice: t(".success")
|
||||
redirect_back_or_to @entry.account, notice: t(".success")
|
||||
end
|
||||
|
||||
def bulk_delete
|
||||
|
|
|
@ -1,41 +1,7 @@
|
|||
class VehiclesController < ApplicationController
|
||||
before_action :set_account, only: :update
|
||||
include AccountableResource
|
||||
|
||||
def create
|
||||
account = Current.family
|
||||
.accounts
|
||||
.create_with_optional_start_balance! \
|
||||
attributes: account_params.except(:start_date, :start_balance),
|
||||
start_date: account_params[:start_date],
|
||||
start_balance: account_params[:start_balance]
|
||||
|
||||
account.sync_later
|
||||
redirect_to account, notice: t(".success")
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update_with_sync!(account_params)
|
||||
redirect_to @account, notice: t(".success")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Current.family.accounts.find(params[:id])
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.require(:account)
|
||||
.permit(
|
||||
:name, :balance, :institution_id, :start_date, :mode, :start_balance, :currency, :accountable_type,
|
||||
accountable_attributes: [
|
||||
:id,
|
||||
:make,
|
||||
:model,
|
||||
:year,
|
||||
:mileage_value,
|
||||
:mileage_unit
|
||||
]
|
||||
)
|
||||
end
|
||||
permitted_accountable_attributes(
|
||||
:id, :make, :model, :year, :mileage_value, :mileage_unit
|
||||
)
|
||||
end
|
||||
|
|
|
@ -12,43 +12,18 @@ module Account::EntriesHelper
|
|||
transfers.map(&:transfer).uniq
|
||||
end
|
||||
|
||||
def entry_icon(entry, is_oldest: false)
|
||||
if is_oldest
|
||||
"keyboard"
|
||||
elsif entry.trend.direction.up?
|
||||
"arrow-up"
|
||||
elsif entry.trend.direction.down?
|
||||
"arrow-down"
|
||||
else
|
||||
"minus"
|
||||
end
|
||||
end
|
||||
|
||||
def entry_style(entry, is_oldest: false)
|
||||
color = is_oldest ? "#D444F1" : entry.trend.color
|
||||
|
||||
mixed_hex_styles(color)
|
||||
end
|
||||
|
||||
def entry_name(entry)
|
||||
if entry.account_trade?
|
||||
trade = entry.account_trade
|
||||
prefix = trade.sell? ? "Sell " : "Buy "
|
||||
generated = prefix + "#{trade.qty.abs} shares of #{trade.security.ticker}"
|
||||
name = entry.name || generated
|
||||
name
|
||||
else
|
||||
entry.name || "Transaction"
|
||||
end
|
||||
end
|
||||
|
||||
def entries_by_date(entries, selectable: true)
|
||||
def entries_by_date(entries, selectable: true, totals: false)
|
||||
entries.group_by(&:date).map do |date, grouped_entries|
|
||||
content = capture do
|
||||
yield grouped_entries
|
||||
# Valuations always go first, then sort by created_at
|
||||
sorted_entries = grouped_entries.sort_by do |entry|
|
||||
[ entry.account_valuation? ? 0 : 1, entry.created_at ]
|
||||
end
|
||||
|
||||
render partial: "account/entries/entry_group", locals: { date:, entries: grouped_entries, content:, selectable: }
|
||||
content = capture do
|
||||
yield sorted_entries
|
||||
end
|
||||
|
||||
render partial: "account/entries/entry_group", locals: { date:, entries: sorted_entries, content:, selectable:, totals: }
|
||||
end.join.html_safe
|
||||
end
|
||||
|
||||
|
|
|
@ -1,12 +1,25 @@
|
|||
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
|
||||
def period_label(period)
|
||||
return "since account creation" if period.date_range.begin.nil?
|
||||
start_date, end_date = period.date_range.first, period.date_range.last
|
||||
|
||||
raise "Unpermitted accountable partial: #{name}" unless permitted_names.include?(name)
|
||||
return "Starting from #{start_date.strftime('%b %d, %Y')}" if end_date.nil?
|
||||
return "Ending at #{end_date.strftime('%b %d, %Y')}" if start_date.nil?
|
||||
|
||||
"accounts/accountables/#{folder}/#{name}"
|
||||
days_apart = (end_date - start_date).to_i
|
||||
|
||||
case days_apart
|
||||
when 1
|
||||
"vs. yesterday"
|
||||
when 7
|
||||
"vs. last week"
|
||||
when 30, 31
|
||||
"vs. last month"
|
||||
when 365, 366
|
||||
"vs. last year"
|
||||
else
|
||||
"from #{start_date.strftime('%b %d, %Y')} to #{end_date.strftime('%b %d, %Y')}"
|
||||
end
|
||||
end
|
||||
|
||||
def summary_card(title:, &block)
|
||||
|
@ -38,62 +51,6 @@ module AccountsHelper
|
|||
class_mapping(accountable_type)[:hex]
|
||||
end
|
||||
|
||||
# Eventually, we'll have an accountable form for each type of accountable, so
|
||||
# this helper is a convenience for now to reuse common logic in the accounts controller
|
||||
def new_account_form_url(account)
|
||||
case account.accountable_type
|
||||
when "Property"
|
||||
properties_path
|
||||
when "Vehicle"
|
||||
vehicles_path
|
||||
when "Loan"
|
||||
loans_path
|
||||
when "CreditCard"
|
||||
credit_cards_path
|
||||
else
|
||||
accounts_path
|
||||
end
|
||||
end
|
||||
|
||||
def edit_account_form_url(account)
|
||||
case account.accountable_type
|
||||
when "Property"
|
||||
property_path(account)
|
||||
when "Vehicle"
|
||||
vehicle_path(account)
|
||||
when "Loan"
|
||||
loan_path(account)
|
||||
when "CreditCard"
|
||||
credit_card_path(account)
|
||||
else
|
||||
account_path(account)
|
||||
end
|
||||
end
|
||||
|
||||
def account_tabs(account)
|
||||
overview_tab = { key: "overview", label: t("accounts.show.overview"), path: account_path(account, tab: "overview"), partial_path: "accounts/overview" }
|
||||
holdings_tab = { key: "holdings", label: t("accounts.show.holdings"), path: account_path(account, tab: "holdings"), route: account_holdings_path(account) }
|
||||
cash_tab = { key: "cash", label: t("accounts.show.cash"), path: account_path(account, tab: "cash"), route: account_cashes_path(account) }
|
||||
value_tab = { key: "valuations", label: t("accounts.show.value"), path: account_path(account, tab: "valuations"), route: account_valuations_path(account) }
|
||||
transactions_tab = { key: "transactions", label: t("accounts.show.transactions"), path: account_path(account, tab: "transactions"), route: account_transactions_path(account) }
|
||||
trades_tab = { key: "trades", label: t("accounts.show.trades"), path: account_path(account, tab: "trades"), route: account_trades_path(account) }
|
||||
|
||||
return [ value_tab ] if account.other_asset? || account.other_liability?
|
||||
return [ overview_tab, value_tab ] if account.property? || account.vehicle?
|
||||
return [ holdings_tab, cash_tab, trades_tab, value_tab ] if account.investment?
|
||||
return [ overview_tab, value_tab, transactions_tab ] if account.loan? || account.credit_card?
|
||||
|
||||
[ value_tab, transactions_tab ]
|
||||
end
|
||||
|
||||
def selected_account_tab(account)
|
||||
available_tabs = account_tabs(account)
|
||||
|
||||
tab = available_tabs.find { |tab| tab[:key] == params[:tab] }
|
||||
|
||||
tab || available_tabs.first
|
||||
end
|
||||
|
||||
def account_groups(period: nil)
|
||||
assets, liabilities = Current.family.accounts.by_group(currency: Current.family.currency, period: period || Period.last_30_days).values_at(:assets, :liabilities)
|
||||
[ assets.children.sort_by(&:name), liabilities.children.sort_by(&:name) ].flatten
|
||||
|
|
|
@ -122,29 +122,6 @@ module ApplicationHelper
|
|||
{ bg_class: bg_class, text_class: text_class, symbol: symbol, icon: icon }
|
||||
end
|
||||
|
||||
def period_label(period)
|
||||
return "since account creation" if period.date_range.begin.nil?
|
||||
start_date, end_date = period.date_range.first, period.date_range.last
|
||||
|
||||
return "Starting from #{start_date.strftime('%b %d, %Y')}" if end_date.nil?
|
||||
return "Ending at #{end_date.strftime('%b %d, %Y')}" if start_date.nil?
|
||||
|
||||
days_apart = (end_date - start_date).to_i
|
||||
|
||||
case days_apart
|
||||
when 1
|
||||
"vs. yesterday"
|
||||
when 7
|
||||
"vs. last week"
|
||||
when 30, 31
|
||||
"vs. last month"
|
||||
when 365, 366
|
||||
"vs. last year"
|
||||
else
|
||||
"from #{start_date.strftime('%b %d, %Y')} to #{end_date.strftime('%b %d, %Y')}"
|
||||
end
|
||||
end
|
||||
|
||||
# Wrapper around I18n.l to support custom date formats
|
||||
def format_date(object, format = :default, options = {})
|
||||
date = object.to_date
|
||||
|
|
|
@ -10,7 +10,8 @@ export default class extends Controller {
|
|||
"bulkEditDrawerTitle",
|
||||
];
|
||||
static values = {
|
||||
resource: String,
|
||||
singularLabel: String,
|
||||
pluralLabel: String,
|
||||
selectedIds: { type: Array, default: [] },
|
||||
};
|
||||
|
||||
|
@ -132,9 +133,11 @@ export default class extends Controller {
|
|||
}
|
||||
|
||||
_pluralizedResourceName() {
|
||||
return `${this.resourceValue}${
|
||||
this.selectedIdsValue.length === 1 ? "" : "s"
|
||||
}`;
|
||||
if (this.selectedIdsValue.length === 1) {
|
||||
return this.singularLabelValue;
|
||||
}
|
||||
|
||||
return this.pluralLabelValue;
|
||||
}
|
||||
|
||||
_updateGroups() {
|
||||
|
|
|
@ -535,7 +535,7 @@ export default class extends Controller {
|
|||
}
|
||||
|
||||
get _d3YScale() {
|
||||
const reductionPercent = this.useLabelsValue ? 0.15 : 0.05;
|
||||
const reductionPercent = this.useLabelsValue ? 0.3 : 0.05;
|
||||
const dataMin = d3.min(this._normalDataPoints, (d) => d.value);
|
||||
const dataMax = d3.max(this._normalDataPoints, (d) => d.value);
|
||||
const padding = (dataMax - dataMin) * reductionPercent;
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
class Account < ApplicationRecord
|
||||
VALUE_MODES = %w[balance transactions]
|
||||
|
||||
include Syncable, Monetizable, Issuable
|
||||
|
||||
validates :name, :balance, :currency, presence: true
|
||||
validates :mode, inclusion: { in: VALUE_MODES }, allow_nil: true
|
||||
|
||||
belongs_to :family
|
||||
belongs_to :institution, optional: true
|
||||
|
@ -34,7 +31,7 @@ class Account < ApplicationRecord
|
|||
|
||||
delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy
|
||||
|
||||
accepts_nested_attributes_for :accountable
|
||||
accepts_nested_attributes_for :accountable, update_only: true
|
||||
|
||||
delegate :value, :series, to: :accountable
|
||||
|
||||
|
@ -61,29 +58,32 @@ class Account < ApplicationRecord
|
|||
grouped_accounts
|
||||
end
|
||||
|
||||
def create_with_optional_start_balance!(attributes:, start_date: nil, start_balance: nil)
|
||||
transaction do
|
||||
attributes[:accountable_attributes] ||= {} # Ensure accountable is created
|
||||
account = new(attributes)
|
||||
def create_and_sync(attributes)
|
||||
attributes[:accountable_attributes] ||= {} # Ensure accountable is created, even if empty
|
||||
account = new(attributes)
|
||||
|
||||
# Always initialize an account with a valuation entry to begin tracking value history
|
||||
account.entries.build \
|
||||
transaction do
|
||||
# Create 2 valuations for new accounts to establish a value history for users to see
|
||||
account.entries.build(
|
||||
name: "Current Balance",
|
||||
date: Date.current,
|
||||
amount: account.balance,
|
||||
currency: account.currency,
|
||||
entryable: Account::Valuation.new
|
||||
|
||||
if start_date.present? && start_balance.present?
|
||||
account.entries.build \
|
||||
date: start_date,
|
||||
amount: start_balance,
|
||||
currency: account.currency,
|
||||
entryable: Account::Valuation.new
|
||||
end
|
||||
)
|
||||
account.entries.build(
|
||||
name: "Initial Balance",
|
||||
date: 1.day.ago.to_date,
|
||||
amount: 0,
|
||||
currency: account.currency,
|
||||
entryable: Account::Valuation.new
|
||||
)
|
||||
|
||||
account.save!
|
||||
account
|
||||
end
|
||||
|
||||
account.sync_later
|
||||
account
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -46,22 +46,40 @@ class Account::Entry < ApplicationRecord
|
|||
amount > 0 && account_transaction?
|
||||
end
|
||||
|
||||
def first_of_type?
|
||||
first_entry = account
|
||||
.entries
|
||||
.where("entryable_type = ?", entryable_type)
|
||||
.order(:date)
|
||||
.first
|
||||
|
||||
first_entry&.id == id
|
||||
end
|
||||
|
||||
def entryable_name_short
|
||||
entryable_type.demodulize.underscore
|
||||
end
|
||||
|
||||
def prior_balance
|
||||
account.balances.find_by(date: date - 1)&.balance || 0
|
||||
end
|
||||
|
||||
def balance_after_entry
|
||||
if account_valuation?
|
||||
Money.new(amount, currency)
|
||||
else
|
||||
new_balance = prior_balance
|
||||
entries_on_entry_date.each do |e|
|
||||
change = e.amount
|
||||
change = account.liability? ? change : -change
|
||||
new_balance += change
|
||||
break if e == self
|
||||
end
|
||||
|
||||
Money.new(new_balance, currency)
|
||||
end
|
||||
end
|
||||
|
||||
def trend
|
||||
@trend ||= create_trend
|
||||
TimeSeries::Trend.new(
|
||||
current: balance_after_entry,
|
||||
previous: Money.new(prior_balance, currency),
|
||||
favorable_direction: account.favorable_direction
|
||||
)
|
||||
end
|
||||
|
||||
def entries_on_entry_date
|
||||
account.entries.where(date: date).order(created_at: :desc)
|
||||
end
|
||||
|
||||
class << self
|
||||
|
@ -216,11 +234,4 @@ class Account::Entry < ApplicationRecord
|
|||
.order(date: :desc)
|
||||
.first
|
||||
end
|
||||
|
||||
def create_trend
|
||||
TimeSeries::Trend.new \
|
||||
current: amount_money,
|
||||
previous: previous_entry&.amount_money,
|
||||
favorable_direction: account.favorable_direction
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,6 +26,12 @@ class Account::Trade < ApplicationRecord
|
|||
qty > 0
|
||||
end
|
||||
|
||||
def name
|
||||
prefix = sell? ? "Sell " : "Buy "
|
||||
generated = prefix + "#{qty.abs} shares of #{security.ticker}"
|
||||
entry.name || generated
|
||||
end
|
||||
|
||||
def unrealized_gain_loss
|
||||
return nil if sell?
|
||||
current_price = security.current_price
|
||||
|
|
|
@ -48,12 +48,20 @@ class Account::Transaction < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def name
|
||||
entry.name || "(no description)"
|
||||
end
|
||||
|
||||
def eod_balance
|
||||
entry.amount_money
|
||||
end
|
||||
|
||||
private
|
||||
def previous_transaction_date
|
||||
self.account
|
||||
.transactions
|
||||
.where("date < ?", date)
|
||||
.order(date: :desc)
|
||||
.first&.date
|
||||
def account
|
||||
entry.account
|
||||
end
|
||||
|
||||
def daily_transactions
|
||||
account.entries.account_transactions
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class Account::Transfer < ApplicationRecord
|
||||
has_many :entries, dependent: :nullify
|
||||
has_many :entries, dependent: :destroy
|
||||
|
||||
validate :net_zero_flows, if: :single_currency_transfer?
|
||||
validate :transaction_count, :from_different_accounts, :all_transactions_marked
|
||||
|
@ -13,17 +13,25 @@ class Account::Transfer < ApplicationRecord
|
|||
end
|
||||
|
||||
def from_name
|
||||
outflow_transaction&.account&.name || I18n.t("account/transfer.from_fallback_name")
|
||||
from_account&.name || I18n.t("account/transfer.from_fallback_name")
|
||||
end
|
||||
|
||||
def to_name
|
||||
inflow_transaction&.account&.name || I18n.t("account/transfer.to_fallback_name")
|
||||
to_account&.name || I18n.t("account/transfer.to_fallback_name")
|
||||
end
|
||||
|
||||
def name
|
||||
I18n.t("account/transfer.name", from_account: from_name, to_account: to_name)
|
||||
end
|
||||
|
||||
def from_account
|
||||
outflow_transaction&.account
|
||||
end
|
||||
|
||||
def to_account
|
||||
inflow_transaction&.account
|
||||
end
|
||||
|
||||
def inflow_transaction
|
||||
entries.find { |e| e.inflow? }
|
||||
end
|
||||
|
@ -32,13 +40,11 @@ class Account::Transfer < ApplicationRecord
|
|||
entries.find { |e| e.outflow? }
|
||||
end
|
||||
|
||||
def destroy_and_remove_marks!
|
||||
def update_entries!(params)
|
||||
transaction do
|
||||
entries.each do |e|
|
||||
e.update! marked_as_transfer: false
|
||||
entries.each do |entry|
|
||||
entry.update!(params)
|
||||
end
|
||||
|
||||
destroy!
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -10,4 +10,44 @@ class Account::Valuation < ApplicationRecord
|
|||
false
|
||||
end
|
||||
end
|
||||
|
||||
def name
|
||||
oldest? ? "Initial balance" : entry.name || "Balance update"
|
||||
end
|
||||
|
||||
def trend
|
||||
@trend ||= create_trend
|
||||
end
|
||||
|
||||
def icon
|
||||
oldest? ? "plus" : entry.trend.icon
|
||||
end
|
||||
|
||||
def color
|
||||
oldest? ? "#D444F1" : entry.trend.color
|
||||
end
|
||||
|
||||
private
|
||||
def oldest?
|
||||
@oldest ||= account.entries.where("date < ?", entry.date).empty?
|
||||
end
|
||||
|
||||
def account
|
||||
@account ||= entry.account
|
||||
end
|
||||
|
||||
def create_trend
|
||||
TimeSeries::Trend.new(
|
||||
current: entry.amount_money,
|
||||
previous: prior_balance&.balance_money,
|
||||
favorable_direction: account.favorable_direction
|
||||
)
|
||||
end
|
||||
|
||||
def prior_balance
|
||||
@prior_balance ||= account.balances
|
||||
.where("date < ?", entry.date)
|
||||
.order(date: :desc)
|
||||
.first
|
||||
end
|
||||
end
|
||||
|
|
|
@ -33,8 +33,4 @@ module Accountable
|
|||
rescue Money::ConversionError
|
||||
TimeSeries.new([])
|
||||
end
|
||||
|
||||
def mode_required?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,4 +16,8 @@ class CreditCard < ApplicationRecord
|
|||
def color
|
||||
"#F13636"
|
||||
end
|
||||
|
||||
def icon
|
||||
"credit-card"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,4 +4,8 @@ class Crypto < ApplicationRecord
|
|||
def color
|
||||
"#737373"
|
||||
end
|
||||
|
||||
def icon
|
||||
"bitcoin"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -34,6 +34,7 @@ class Demo::Generator
|
|||
create_investment_account!
|
||||
create_house_and_mortgage!
|
||||
create_car_and_loan!
|
||||
create_other_accounts!
|
||||
|
||||
puts "accounts created"
|
||||
puts "Demo data loaded successfully!"
|
||||
|
@ -50,7 +51,7 @@ class Demo::Generator
|
|||
family = Family.find_by(id: family_id)
|
||||
family.destroy! if family
|
||||
|
||||
Family.create!(id: family_id, name: "Demo Family").tap(&:reload)
|
||||
Family.create!(id: family_id, name: "Demo Family", stripe_subscription_status: "active").tap(&:reload)
|
||||
end
|
||||
|
||||
def clear_data!
|
||||
|
@ -273,6 +274,20 @@ class Demo::Generator
|
|||
currency: "USD"
|
||||
end
|
||||
|
||||
def create_other_accounts!
|
||||
family.accounts.create! \
|
||||
accountable: OtherAsset.new,
|
||||
name: "Other Asset",
|
||||
balance: 10000,
|
||||
currency: "USD"
|
||||
|
||||
family.accounts.create! \
|
||||
accountable: OtherLiability.new,
|
||||
name: "Other Liability",
|
||||
balance: 5000,
|
||||
currency: "USD"
|
||||
end
|
||||
|
||||
def create_transaction!(attributes = {})
|
||||
entry_attributes = attributes.except(:category, :tags, :merchant)
|
||||
transaction_attributes = attributes.slice(:category, :tags, :merchant)
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
class Depository < ApplicationRecord
|
||||
include Accountable
|
||||
|
||||
SUBTYPES = [
|
||||
[ "Checking", "checking" ],
|
||||
[ "Savings", "savings" ]
|
||||
].freeze
|
||||
|
||||
def color
|
||||
"#875BF7"
|
||||
end
|
||||
|
||||
def icon
|
||||
"landmark"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -50,4 +50,8 @@ class Investment < ApplicationRecord
|
|||
def color
|
||||
"#1570EF"
|
||||
end
|
||||
|
||||
def icon
|
||||
"line-chart"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,4 +20,8 @@ class Loan < ApplicationRecord
|
|||
def color
|
||||
"#D444F1"
|
||||
end
|
||||
|
||||
def icon
|
||||
"hand-coins"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@ class OtherAsset < ApplicationRecord
|
|||
"#12B76A"
|
||||
end
|
||||
|
||||
def mode_required?
|
||||
false
|
||||
def icon
|
||||
"plus"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@ class OtherLiability < ApplicationRecord
|
|||
"#737373"
|
||||
end
|
||||
|
||||
def mode_required?
|
||||
false
|
||||
def icon
|
||||
"minus"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
class Period
|
||||
attr_reader :name, :date_range
|
||||
|
||||
def self.find_by_name(name)
|
||||
INDEX[name]
|
||||
end
|
||||
class << self
|
||||
def from_param(param)
|
||||
find_by_name(param) || self.last_30_days
|
||||
end
|
||||
|
||||
def self.names
|
||||
INDEX.keys.sort
|
||||
def find_by_name(name)
|
||||
INDEX[name]
|
||||
end
|
||||
|
||||
def names
|
||||
INDEX.keys.sort
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(name: "custom", date_range:)
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
class Property < ApplicationRecord
|
||||
include Accountable
|
||||
|
||||
SUBTYPES = [
|
||||
[ "Single Family Home", "single_family_home" ],
|
||||
[ "Multi-Family Home", "multi_family_home" ],
|
||||
[ "Condominium", "condominium" ],
|
||||
[ "Townhouse", "townhouse" ],
|
||||
[ "Investment Property", "investment_property" ]
|
||||
]
|
||||
|
||||
has_one :address, as: :addressable, dependent: :destroy
|
||||
|
||||
accepts_nested_attributes_for :address
|
||||
|
@ -23,8 +31,8 @@ class Property < ApplicationRecord
|
|||
"#06AED4"
|
||||
end
|
||||
|
||||
def mode_required?
|
||||
false
|
||||
def icon
|
||||
"home"
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -35,9 +35,19 @@ class TimeSeries::Trend
|
|||
end
|
||||
end
|
||||
|
||||
def icon
|
||||
if direction.flat?
|
||||
"minus"
|
||||
elsif direction.up?
|
||||
"arrow-up"
|
||||
else
|
||||
"arrow-down"
|
||||
end
|
||||
end
|
||||
|
||||
def value
|
||||
if previous.nil?
|
||||
current.is_a?(Money) ? Money.new(0) : 0
|
||||
current.is_a?(Money) ? Money.new(0, current.currency) : 0
|
||||
else
|
||||
current - previous
|
||||
end
|
||||
|
|
|
@ -19,8 +19,8 @@ class Vehicle < ApplicationRecord
|
|||
"#F23E94"
|
||||
end
|
||||
|
||||
def mode_required?
|
||||
false
|
||||
def icon
|
||||
"car-front"
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<%# locals: (entry:, **opts) %>
|
||||
<%# locals: (entry:, selectable: true, show_balance: false, origin: nil) %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(entry) do %>
|
||||
<%= render partial: entry.entryable.to_partial_path, locals: { entry: entry, **opts } %>
|
||||
<%= render partial: entry.entryable.to_partial_path, locals: { entry:, selectable:, show_balance:, origin: } %>
|
||||
<% end %>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<%# locals: (date:, entries:, content:, selectable:) %>
|
||||
<%# locals: (date:, entries:, content:, selectable:, totals: false) %>
|
||||
<div id="entry-group-<%= date %>" class="bg-gray-25 rounded-xl p-1 w-full" data-bulk-select-target="group">
|
||||
<div class="py-2 px-4 flex items-center justify-between font-medium text-xs text-gray-500">
|
||||
<div class="flex pl-0.5 items-center gap-4">
|
||||
|
@ -16,7 +16,9 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<%= totals_by_currency(collection: entries, money_method: :amount_money, negate: true) %>
|
||||
<% if totals %>
|
||||
<%= totals_by_currency(collection: entries, money_method: :amount_money, negate: true) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="bg-white shadow-xs rounded-md border border-alpha-black-25 divide-y divide-alpha-black-50">
|
||||
<%= content %>
|
||||
|
|
15
app/views/account/entries/_selection_bar.html.erb
Normal file
15
app/views/account/entries/_selection_bar.html.erb
Normal file
|
@ -0,0 +1,15 @@
|
|||
<div class="fixed bottom-6 z-10 flex items-center justify-between rounded-xl bg-gray-900 px-4 text-sm text-white w-[420px] py-1.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= check_box_tag "entry_selection", 1, true, class: "maybe-checkbox maybe-checkbox--dark", data: { action: "bulk-select#deselectAll" } %>
|
||||
|
||||
<p data-bulk-select-target="selectionBarText"></p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1 text-gray-500">
|
||||
<%= form_with url: bulk_delete_transactions_path, data: { turbo_confirm: true, turbo_frame: "_top" } do %>
|
||||
<button type="button" data-bulk-select-scope-param="bulk_delete" data-action="bulk-select#submitBulkRequest" class="p-1.5 group hover:bg-gray-700 flex items-center justify-center rounded-md" title="Delete">
|
||||
<%= lucide_icon "trash-2", class: "w-5 group-hover:text-white" %>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
88
app/views/account/entries/index.html.erb
Normal file
88
app/views/account/entries/index.html.erb
Normal file
|
@ -0,0 +1,88 @@
|
|||
<%= turbo_frame_tag dom_id(@account, "entries") do %>
|
||||
<div class="bg-white p-5 border border-alpha-black-25 rounded-xl shadow-xs">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<%= tag.h2 t(".title"), class: "font-medium text-lg" %>
|
||||
<div data-controller="menu" data-testid="activity-menu">
|
||||
<button class="btn btn--secondary flex items-center gap-2" data-menu-target="button">
|
||||
<%= lucide_icon("plus", class: "w-4 h-4") %>
|
||||
<%= tag.span t(".new") %>
|
||||
</button>
|
||||
<div data-menu-target="content" class="z-10 hidden bg-white rounded-lg border border-alpha-black-25 shadow-xs p-1">
|
||||
<%= link_to new_account_valuation_path(@account), data: { turbo_frame: :modal }, class: "block p-2 rounded-lg hover:bg-gray-50 flex items-center gap-2" do %>
|
||||
<%= lucide_icon("circle-dollar-sign", class: "text-gray-500 w-5 h-5") %>
|
||||
<%= tag.span t(".new_balance"), class: "text-sm" %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to @account.investment? ? new_account_trade_path(@account) : new_transaction_path(account_id: @account.id), data: { turbo_frame: :modal }, class: "block p-2 rounded-lg hover:bg-gray-50 flex items-center gap-2" do %>
|
||||
<%= lucide_icon("credit-card", class: "text-gray-500 w-5 h-5") %>
|
||||
<%= tag.span t(".new_transaction"), class: "text-sm" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if @entries.empty? %>
|
||||
<p class="text-gray-500 text-sm p-4"><%= t(".no_entries") %></p>
|
||||
<% else %>
|
||||
|
||||
<div>
|
||||
<%= form_with url: account_entries_path(@account),
|
||||
id: "entries-search",
|
||||
scope: :q,
|
||||
method: :get,
|
||||
data: { controller: "auto-submit-form" } do |form| %>
|
||||
<div class="flex gap-2 mb-4">
|
||||
<div class="grow">
|
||||
<div class="flex items-center px-3 py-2 gap-2 border border-gray-200 rounded-lg focus-within:ring-gray-100 focus-within:border-gray-900">
|
||||
<%= lucide_icon("search", class: "w-5 h-5 text-gray-500") %>
|
||||
<%= form.search_field :search,
|
||||
placeholder: "Search entries by name",
|
||||
value: @q[:search],
|
||||
class: "form-field__input placeholder:text-sm placeholder:text-gray-500",
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= tag.div id: dom_id(@account, "entries_bulk_select"),
|
||||
data: {
|
||||
controller: "bulk-select",
|
||||
bulk_select_singular_label_value: t(".entry"),
|
||||
bulk_select_plural_label_value: t(".entries")
|
||||
} do %>
|
||||
<div id="entry-selection-bar" data-bulk-select-target="selectionBar" class="flex justify-center hidden">
|
||||
<%= render "account/entries/selection_bar" %>
|
||||
</div>
|
||||
|
||||
<div class="grid bg-gray-25 rounded-xl grid-cols-12 items-center uppercase text-xs font-medium text-gray-500 px-5 py-3 mb-4">
|
||||
<div class="pl-0.5 col-span-8 flex items-center gap-4">
|
||||
<%= check_box_tag "selection_entry",
|
||||
class: "maybe-checkbox maybe-checkbox--light",
|
||||
data: { action: "bulk-select#togglePageSelection" } %>
|
||||
<p><%= t(".date") %></p>
|
||||
</div>
|
||||
<%= tag.p t(".amount"), class: "col-span-2 justify-self-end" %>
|
||||
<%= tag.p t(".balance"), class: "col-span-2 justify-self-end" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="rounded-tl-lg rounded-tr-lg bg-white border-alpha-black-25 shadow-xs">
|
||||
|
||||
<div class="space-y-4">
|
||||
<%= entries_by_date(@entries) do |entries| %>
|
||||
<%= render entries, show_balance: true, origin: "account" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-white rounded-bl-lg rounded-br-lg">
|
||||
<%= render "pagination", pagy: @pagy %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
|
@ -1,4 +1,4 @@
|
|||
<%# locals: (entry:, selectable: true, **opts) %>
|
||||
<%# locals: (entry:, selectable: true, show_balance: false, origin: nil) %>
|
||||
|
||||
<% trade, account = entry.account_trade, entry.account %>
|
||||
|
||||
|
@ -13,14 +13,14 @@
|
|||
<div class="max-w-full">
|
||||
<%= tag.div class: ["flex items-center gap-2"] do %>
|
||||
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gray-600/5 text-gray-600">
|
||||
<%= entry_name(entry).first.upcase %>
|
||||
<%= trade.name.first.upcase %>
|
||||
</div>
|
||||
|
||||
<div class="truncate text-gray-900">
|
||||
<% if entry.new_record? %>
|
||||
<%= content_tag :p, entry_name(entry) %>
|
||||
<%= content_tag :p, trade.name %>
|
||||
<% else %>
|
||||
<%= link_to entry_name(entry),
|
||||
<%= link_to trade.name,
|
||||
account_entry_path(account, entry),
|
||||
data: { turbo_frame: "drawer", turbo_prefetch: false },
|
||||
class: "hover:underline hover:text-gray-800" %>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<%# locals: (entry:, selectable: true, editable: true, short: false, show_tags: false, **opts) %>
|
||||
<%# locals: (entry:, selectable: true, show_balance: false, origin: nil) %>
|
||||
<% transaction, account = entry.account_transaction, entry.account %>
|
||||
|
||||
<div class="grid grid-cols-12 items-center text-gray-900 text-sm font-medium p-4">
|
||||
<% name_col_span = unconfirmed_transfer?(entry) ? "col-span-10" : short ? "col-span-6" : "col-span-4" %>
|
||||
<div class="pr-10 flex items-center gap-4 <%= name_col_span %>">
|
||||
<div class="grid grid-cols-12 items-center <%= entry.excluded ? "text-gray-400 bg-gray-25" : "text-gray-900" %> text-sm font-medium p-4">
|
||||
<div class="pr-10 flex items-center gap-4 col-span-6">
|
||||
<% if selectable %>
|
||||
<%= check_box_tag dom_id(entry, "selection"),
|
||||
class: "maybe-checkbox maybe-checkbox--light",
|
||||
|
@ -13,15 +12,15 @@
|
|||
<div class="max-w-full">
|
||||
<%= content_tag :div, class: ["flex items-center gap-2"] do %>
|
||||
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gray-600/5 text-gray-600">
|
||||
<%= entry_name(entry).first.upcase %>
|
||||
<%= transaction.name.first.upcase %>
|
||||
</div>
|
||||
|
||||
<div class="truncate text-gray-900">
|
||||
<% if entry.new_record? || !editable %>
|
||||
<%= content_tag :p, entry.name %>
|
||||
<div class="truncate">
|
||||
<% if entry.new_record? %>
|
||||
<%= content_tag :p, transaction.name %>
|
||||
<% else %>
|
||||
<%= link_to entry_name(entry),
|
||||
account_entry_path(account, entry),
|
||||
<%= link_to transaction.name,
|
||||
entry.transfer.present? ? account_transfer_path(entry.transfer, origin:) : account_entry_path(account, entry, origin:),
|
||||
data: { turbo_frame: "drawer", turbo_prefetch: false },
|
||||
class: "hover:underline hover:text-gray-800" %>
|
||||
<% end %>
|
||||
|
@ -30,46 +29,25 @@
|
|||
</div>
|
||||
|
||||
<% if unconfirmed_transfer?(entry) %>
|
||||
<% if editable %>
|
||||
<%= form_with url: unmark_transfers_transactions_path, class: "flex items-center", data: {
|
||||
turbo_confirm: {
|
||||
title: t(".remove_transfer"),
|
||||
body: t(".remove_transfer_body"),
|
||||
accept: t(".remove_transfer_confirm"),
|
||||
},
|
||||
turbo_frame: "_top"
|
||||
} do |f| %>
|
||||
<%= f.hidden_field "bulk_update[entry_ids][]", value: entry.id %>
|
||||
<%= f.button class: "flex items-center justify-center group", title: "Remove transfer" do %>
|
||||
<%= lucide_icon "arrow-left-right", class: "group-hover:hidden text-gray-500 w-4 h-4" %>
|
||||
<%= lucide_icon "unlink", class: "hidden group-hover:inline-block text-gray-900 w-4 h-4" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= lucide_icon "arrow-left-right", class: "text-gray-500 w-4 h-4" %>
|
||||
<% end %>
|
||||
<%= render "account/transfers/transfer_toggle", entry: entry %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% unless entry.marked_as_transfer? %>
|
||||
<% unless short %>
|
||||
<div class="flex items-center gap-1 <%= show_tags ? "col-span-6" : "col-span-3" %>">
|
||||
<% if editable %>
|
||||
<%= render "categories/menu", transaction: transaction %>
|
||||
<% else %>
|
||||
<%= render "categories/badge", category: transaction.category %>
|
||||
<% end %>
|
||||
|
||||
<% if show_tags %>
|
||||
<% transaction.tags.each do |tag| %>
|
||||
<%= render partial: "tags/badge", locals: { tag: tag } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if entry.transfer.present? %>
|
||||
<% unless show_balance %>
|
||||
<div class="col-span-2"></div>
|
||||
<% end %>
|
||||
|
||||
<% unless show_tags %>
|
||||
<%= tag.div class: short ? "col-span-4" : "col-span-3" do %>
|
||||
<div class="col-span-2">
|
||||
<%= render "account/transfers/account_logos", transfer: entry.transfer, outflow: entry.outflow? %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="flex items-center gap-1 col-span-2">
|
||||
<%= render "categories/menu", transaction: transaction, origin: origin %>
|
||||
</div>
|
||||
|
||||
<% unless show_balance %>
|
||||
<%= tag.div class: "col-span-2" do %>
|
||||
<% if entry.new_record? %>
|
||||
<%= tag.p account.name %>
|
||||
<% else %>
|
||||
|
@ -87,4 +65,10 @@
|
|||
format_money(-entry.amount_money),
|
||||
class: ["text-green-600": entry.inflow?] %>
|
||||
</div>
|
||||
|
||||
<% if show_balance %>
|
||||
<div class="col-span-2 justify-self-end">
|
||||
<%= tag.p format_money(entry.trend.current), class: "font-medium text-sm text-gray-900" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<% entry, transaction, account = @entry, @entry.account_transaction, @entry.account %>
|
||||
|
||||
<% origin = params[:origin] %>
|
||||
|
||||
<%= drawer do %>
|
||||
<header class="mb-4 space-y-1">
|
||||
<div class="flex items-center gap-4">
|
||||
|
@ -31,6 +33,7 @@
|
|||
url: account_transaction_path(account, entry),
|
||||
class: "space-y-2",
|
||||
data: { controller: "auto-submit-form" } do |f| %>
|
||||
<%= f.hidden_field :origin, value: origin %>
|
||||
<%= f.text_field :name,
|
||||
label: t(".name_label"),
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
|
@ -73,6 +76,7 @@
|
|||
url: account_transaction_path(account, entry),
|
||||
class: "space-y-2",
|
||||
data: { controller: "auto-submit-form" } do |f| %>
|
||||
<%= f.hidden_field :origin, value: origin %>
|
||||
<%= f.fields_for :entryable do |ef| %>
|
||||
<% unless entry.marked_as_transfer? %>
|
||||
<%= ef.collection_select :category_id,
|
||||
|
@ -110,6 +114,7 @@
|
|||
url: account_transaction_path(account, entry),
|
||||
class: "space-y-2",
|
||||
data: { controller: "auto-submit-form" } do |f| %>
|
||||
<%= f.hidden_field :origin, value: origin %>
|
||||
<%= f.text_area :notes,
|
||||
label: t(".note_label"),
|
||||
placeholder: t(".note_placeholder"),
|
||||
|
@ -128,6 +133,7 @@
|
|||
url: account_transaction_path(account, entry),
|
||||
class: "p-3",
|
||||
data: { controller: "auto-submit-form" } do |f| %>
|
||||
<%= f.hidden_field :origin, value: origin %>
|
||||
<div class="flex cursor-pointer items-center gap-2 justify-between">
|
||||
<div class="text-sm space-y-1">
|
||||
<h4 class="text-gray-900"><%= t(".exclude_title") %></h4>
|
||||
|
@ -138,28 +144,26 @@
|
|||
<%= f.check_box :excluded,
|
||||
class: "sr-only peer",
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
<label for="account_entry_entryable_attributes_excluded"
|
||||
<label for="account_entry_excluded"
|
||||
class="maybe-switch"></label>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Delete Transaction Form -->
|
||||
<% unless entry.marked_as_transfer? %>
|
||||
<div class="flex items-center justify-between gap-2 p-3">
|
||||
<div class="text-sm space-y-1">
|
||||
<h4 class="text-gray-900"><%= t(".delete_title") %></h4>
|
||||
<p class="text-gray-500"><%= t(".delete_subtitle") %></p>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2 p-3">
|
||||
<div class="text-sm space-y-1">
|
||||
<h4 class="text-gray-900"><%= t(".delete_title") %></h4>
|
||||
<p class="text-gray-500"><%= t(".delete_subtitle") %></p>
|
||||
</div>
|
||||
|
||||
<%= button_to t(".delete"),
|
||||
<%= button_to t(".delete"),
|
||||
account_entry_path(account, entry),
|
||||
method: :delete,
|
||||
class: "rounded-lg px-3 py-2 text-red-500 text-sm
|
||||
font-medium border border-alpha-black-200",
|
||||
data: { turbo_confirm: true, turbo_frame: "_top" } %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
25
app/views/account/transfers/_account_logos.html.erb
Normal file
25
app/views/account/transfers/_account_logos.html.erb
Normal file
|
@ -0,0 +1,25 @@
|
|||
<%# locals: (transfer:, outflow: false) %>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<% if outflow %>
|
||||
<%= link_to transfer.from_account, data: { turbo_frame: :_top }, class: "hover:opacity-90" do %>
|
||||
<%= circle_logo(transfer.from_name[0].upcase, size: "sm") %>
|
||||
<% end %>
|
||||
|
||||
<%= lucide_icon "arrow-right", class: "text-gray-500 w-4 h-4" %>
|
||||
|
||||
<%= link_to transfer.to_account, data: { turbo_frame: :_top }, class: "hover:opacity-90" do %>
|
||||
<%= circle_logo(transfer.to_name[0].upcase, size: "sm") %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= link_to transfer.to_account, data: { turbo_frame: :_top }, class: "hover:opacity-90" do %>
|
||||
<%= circle_logo(transfer.to_name[0].upcase, size: "sm") %>
|
||||
<% end %>
|
||||
|
||||
<%= lucide_icon "arrow-left", class: "text-gray-500 w-4 h-4" %>
|
||||
|
||||
<%= link_to transfer.from_account, data: { turbo_frame: :_top }, class: "hover:opacity-90" do %>
|
||||
<%= circle_logo(transfer.from_name[0].upcase, size: "sm") %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
|
@ -1,49 +0,0 @@
|
|||
<%# locals: (transfer:, selectable: true, editable: true, short: false, **opts) %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(transfer) do %>
|
||||
<div class="grid grid-cols-12 items-center text-gray-900 text-sm font-medium p-4">
|
||||
<div class="col-span-7 flex items-center">
|
||||
<% if selectable %>
|
||||
<%= check_box_tag dom_id(transfer, "selection"),
|
||||
disabled: true,
|
||||
class: "mr-3 cursor-not-allowed maybe-checkbox maybe-checkbox--light" %>
|
||||
<% end %>
|
||||
|
||||
<%= tag.div class: short ? "max-w-[250px]" : "max-w-[325px]" do %>
|
||||
<div class="flex items-center gap-2 <%= selectable ? "" : "pl-8" %>">
|
||||
<%= circle_logo(transfer.from_name[0].upcase) %>
|
||||
|
||||
<%= tag.p transfer.name, class: "truncate text-gray-900" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= button_to account_transfer_path(transfer),
|
||||
method: :delete,
|
||||
class: "ml-2 flex items-center group/transfer hover:bg-gray-50 rounded-md p-1",
|
||||
data: {
|
||||
turbo_frame: "_top",
|
||||
turbo_confirm: {
|
||||
title: t(".remove_title"),
|
||||
body: t(".remove_body"),
|
||||
confirm: t(".remove_confirm")
|
||||
}
|
||||
} do %>
|
||||
|
||||
<%= lucide_icon "link-2", class: "group-hover/transfer:hidden w-4 h-4 text-gray-500" %>
|
||||
<%= lucide_icon "unlink", class: "group-hover/transfer:inline-block hidden w-4 h-4 text-gray-500" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% unless short %>
|
||||
<div class="col-span-3 flex items-center gap-2">
|
||||
<%= circle_logo(transfer.from_name[0].upcase, size: "sm") %>
|
||||
<span class="text-gray-500 font-medium">→</span>
|
||||
<%= circle_logo(transfer.to_name[0].upcase, size: "sm") %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="ml-auto <%= short ? "col-span-5" : "col-span-2" %>">
|
||||
<%= tag.p format_money(transfer.amount_money), class: "font-medium" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
16
app/views/account/transfers/_transfer_toggle.html.erb
Normal file
16
app/views/account/transfers/_transfer_toggle.html.erb
Normal file
|
@ -0,0 +1,16 @@
|
|||
<%# locals: (entry:) %>
|
||||
|
||||
<%= form_with url: unmark_transfers_transactions_path, class: "flex items-center", data: {
|
||||
turbo_confirm: {
|
||||
title: t(".remove_transfer"),
|
||||
body: t(".remove_transfer_body"),
|
||||
accept: t(".remove_transfer_confirm"),
|
||||
},
|
||||
turbo_frame: "_top"
|
||||
} do |f| %>
|
||||
<%= f.hidden_field "bulk_update[entry_ids][]", value: entry.id %>
|
||||
<%= f.button class: "flex items-center justify-center group", title: "Remove transfer" do %>
|
||||
<%= lucide_icon "arrow-left-right", class: "group-hover:hidden text-gray-500 w-4 h-4" %>
|
||||
<%= lucide_icon "unlink", class: "hidden group-hover:inline-block text-gray-900 w-4 h-4" %>
|
||||
<% end %>
|
||||
<% end %>
|
121
app/views/account/transfers/show.html.erb
Normal file
121
app/views/account/transfers/show.html.erb
Normal file
|
@ -0,0 +1,121 @@
|
|||
<%= drawer do %>
|
||||
<header class="mb-4 space-y-1">
|
||||
<div class="flex items-center gap-4">
|
||||
<h3 class="font-medium">
|
||||
<span class="text-2xl">
|
||||
<%= format_money @transfer.amount_money %>
|
||||
</span>
|
||||
|
||||
<span class="text-lg text-gray-500">
|
||||
<%= @transfer.amount_money.currency.iso_code %>
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<%= lucide_icon "arrow-left-right", class: "text-gray-500 mt-1 w-5 h-5" %>
|
||||
</div>
|
||||
|
||||
<span class="text-sm text-gray-500">
|
||||
<%= @transfer.name %>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div class="space-y-2">
|
||||
<!-- Overview Section -->
|
||||
<%= disclosure t(".overview") do %>
|
||||
<div class="pb-4 px-3 pt-2 text-sm space-y-3 text-gray-900">
|
||||
<div class="space-y-3">
|
||||
<dl class="flex items-center gap-2 justify-between">
|
||||
<dt class="text-gray-500">To</dt>
|
||||
<dd class="flex items-center gap-2 font-medium">
|
||||
<%= render "accounts/logo", account: @transfer.inflow_transaction.account, size: "sm" %>
|
||||
<%= @transfer.to_name %>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<dl class="flex items-center gap-2 justify-between">
|
||||
<dt class="text-gray-500">Date</dt>
|
||||
<dd class="font-medium"><%= l(@transfer.date, format: :long) %></dd>
|
||||
</dl>
|
||||
|
||||
<dl class="flex items-center gap-2 justify-between">
|
||||
<dt class="text-gray-500">Amount</dt>
|
||||
<dd class="font-medium text-red-500"><%= format_money -@transfer.amount_money %></dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="bg-alpha-black-100 h-px my-2"></div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<dl class="flex items-center gap-2 justify-between">
|
||||
<dt class="text-gray-500">From</dt>
|
||||
<dd class="flex items-center gap-2 font-medium">
|
||||
<%= render "accounts/logo", account: @transfer.outflow_transaction.account, size: "sm" %>
|
||||
<%= @transfer.from_name %>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<dl class="flex items-center gap-2 justify-between">
|
||||
<dt class="text-gray-500">Date</dt>
|
||||
<dd class="font-medium"><%= l(@transfer.date, format: :long) %></dd>
|
||||
</dl>
|
||||
|
||||
<dl class="flex items-center gap-2 justify-between">
|
||||
<dt class="text-gray-500">Amount</dt>
|
||||
<dd class="font-medium text-green-500">+<%= format_money @transfer.amount_money %></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Details Section -->
|
||||
<%= disclosure t(".details") do %>
|
||||
<%= styled_form_with model: @transfer,
|
||||
data: { controller: "auto-submit-form" } do |f| %>
|
||||
<%= f.text_area :notes,
|
||||
label: t(".note_label"),
|
||||
placeholder: t(".note_placeholder"),
|
||||
value: @transfer.outflow_transaction.notes,
|
||||
rows: 5,
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<!-- Settings Section -->
|
||||
<%= disclosure t(".settings") do %>
|
||||
<div class="pb-4">
|
||||
<%= styled_form_with model: @transfer,
|
||||
class: "p-3", data: { controller: "auto-submit-form" } do |f| %>
|
||||
<div class="flex cursor-pointer items-center gap-2 justify-between">
|
||||
<div class="text-sm space-y-1">
|
||||
<h4 class="text-gray-900"><%= t(".exclude_title") %></h4>
|
||||
<p class="text-gray-500"><%= t(".exclude_subtitle") %></p>
|
||||
</div>
|
||||
|
||||
<div class="relative inline-block select-none">
|
||||
<%= f.check_box :excluded,
|
||||
checked: @transfer.inflow_transaction.excluded,
|
||||
class: "sr-only peer",
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
<label for="account_transfer_excluded"
|
||||
class="maybe-switch"></label>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="flex items-center justify-between gap-2 p-3">
|
||||
<div class="text-sm space-y-1">
|
||||
<h4 class="text-gray-900"><%= t(".delete_title") %></h4>
|
||||
<p class="text-gray-500"><%= t(".delete_subtitle") %></p>
|
||||
</div>
|
||||
|
||||
<%= button_to t(".delete"),
|
||||
account_transfer_path(@transfer),
|
||||
method: :delete,
|
||||
class: "rounded-lg px-3 py-2 text-red-500 text-sm
|
||||
font-medium border border-alpha-black-200",
|
||||
data: { turbo_confirm: true, turbo_frame: "_top" } %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
|
@ -1,23 +1,13 @@
|
|||
<%# locals: (entry:) %>
|
||||
|
||||
<%= form_with model: [entry.account, entry],
|
||||
data: { turbo_frame: "_top" },
|
||||
url: entry.new_record? ? account_valuations_path(entry.account) : account_entry_path(entry.account, entry) do |f| %>
|
||||
<div class="grid grid-cols-10 p-4 items-center">
|
||||
<div class="col-span-7 flex items-center gap-4">
|
||||
<div class="w-8 h-8 rounded-full p-1.5 flex items-center justify-center bg-gray-500/5">
|
||||
<%= lucide_icon("pencil-line", class: "w-4 h-4 text-gray-500") %>
|
||||
</div>
|
||||
<div class="w-full flex items-center justify-between gap-2">
|
||||
<%= f.date_field :date, required: "required", min: Account::Entry.min_supported_date, max: Date.current, value: Date.current, class: "border border-alpha-black-200 bg-white rounded-lg shadow-xs min-w-[200px] px-3 py-1.5 text-gray-900 text-sm" %>
|
||||
<%= f.number_field :amount, required: "required", placeholder: "0.00", step: "0.01", class: "bg-white border border-alpha-black-200 rounded-lg shadow-xs text-gray-900 text-sm px-3 py-1.5 text-right" %>
|
||||
<%= f.hidden_field :currency, value: entry.account.currency %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-3 flex gap-2 justify-end items-center">
|
||||
<%= link_to t(".cancel"), account_valuations_path(entry.account), class: "text-sm text-gray-900 hover:text-gray-800 font-medium px-3 py-1.5" %>
|
||||
<%= f.submit class: "bg-gray-50 rounded-lg font-medium px-3 py-1.5 cursor-pointer hover:bg-gray-100 text-sm" %>
|
||||
</div>
|
||||
<%= styled_form_with model: [entry.account, entry],
|
||||
url: entry.new_record? ? account_valuations_path(entry.account) : account_entry_path(entry.account, entry),
|
||||
class: "space-y-4",
|
||||
data: { turbo: false } do |form| %>
|
||||
<div class="space-y-3">
|
||||
<%= form.date_field :date, label: true, required: true, value: Date.current, min: Account::Entry.min_supported_date, max: Date.current %>
|
||||
<%= form.money_field :amount, label: t(".amount"), required: true, default_currency: Current.family.currency %>
|
||||
</div>
|
||||
|
||||
<%= form.submit t(".submit") %>
|
||||
<% end %>
|
||||
|
|
|
@ -1,50 +1,39 @@
|
|||
<%# locals: (entry:, **opts) %>
|
||||
<%# locals: (entry:, selectable: true, show_balance: false, origin: nil) %>
|
||||
|
||||
<% account = entry.account %>
|
||||
<% valuation = entry.account_valuation %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(entry) do %>
|
||||
<% is_oldest = entry.first_of_type? %>
|
||||
<div class="p-4 grid grid-cols-12 items-center text-gray-900 text-sm font-medium">
|
||||
<div class="col-span-8 flex items-center gap-4">
|
||||
<% if selectable %>
|
||||
<%= check_box_tag dom_id(entry, "selection"),
|
||||
class: "maybe-checkbox maybe-checkbox--light",
|
||||
data: { id: entry.id, "bulk-select-target": "row", action: "bulk-select#toggleRowSelection" } %>
|
||||
<% end %>
|
||||
|
||||
<div class="p-4 grid grid-cols-10 items-center">
|
||||
<div class="col-span-5 flex items-center gap-4">
|
||||
<%= tag.div class: "w-8 h-8 rounded-full p-1.5 flex items-center justify-center", style: entry_style(entry, is_oldest:).html_safe do %>
|
||||
<%= lucide_icon entry_icon(entry, is_oldest:), class: "w-4 h-4" %>
|
||||
<div class="flex items-center gap-3">
|
||||
<%= tag.div class: "w-8 h-8 rounded-full p-1.5 flex items-center justify-center", style: mixed_hex_styles(valuation.color) do %>
|
||||
<%= lucide_icon valuation.icon, class: "w-4 h-4" %>
|
||||
<% end %>
|
||||
|
||||
<div class="text-sm">
|
||||
<%= tag.p entry.date, class: "text-gray-900 font-medium" %>
|
||||
<%= tag.p is_oldest ? t(".start_balance") : t(".value_update"), class: "text-gray-500" %>
|
||||
<div class="truncate text-gray-900">
|
||||
<% if entry.new_record? %>
|
||||
<%= content_tag :p, entry.name %>
|
||||
<% else %>
|
||||
<%= link_to valuation.name,
|
||||
account_entry_path(account, entry),
|
||||
data: { turbo_frame: "drawer", turbo_prefetch: false },
|
||||
class: "hover:underline hover:text-gray-800" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 justify-self-end">
|
||||
<%= tag.p format_money(entry.amount_money), class: "font-medium text-sm text-gray-900" %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 justify-self-end font-medium text-sm" style="color: <%= entry.trend.color %>">
|
||||
<% if entry.trend.direction.flat? %>
|
||||
<%= tag.span t(".no_change"), class: "text-gray-500" %>
|
||||
<% else %>
|
||||
<%= tag.span format_money(entry.trend.value) %>
|
||||
<%= tag.span "(#{entry.trend.percent}%)" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-1 justify-self-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">
|
||||
<%= contextual_menu_modal_action_item t(".edit_entry"), edit_account_entry_path(account, entry), turbo_frame: dom_id(entry) %>
|
||||
|
||||
<%= contextual_menu_destructive_item t(".delete_entry"),
|
||||
account_entry_path(account, entry),
|
||||
turbo_frame: "_top",
|
||||
turbo_confirm: {
|
||||
title: t(".confirm_title"),
|
||||
body: t(".confirm_body_html"),
|
||||
accept: t(".confirm_accept")
|
||||
} %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="col-span-2 justify-self-end font-medium text-sm" style="color: <%= valuation.color %>">
|
||||
<%= tag.span format_money(entry.trend.value) %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 justify-self-end">
|
||||
<%= tag.p format_money(entry.amount_money), class: "font-medium text-sm text-gray-900" %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
<%= turbo_frame_tag dom_id(@entry) do %>
|
||||
<%= render "account/valuations/form", entry: @entry %>
|
||||
<div class="h-px bg-alpha-black-50 ml-20 mr-4"></div>
|
||||
<%= modal_form_wrapper title: t(".title") do %>
|
||||
<%= render "form", entry: @entry %>
|
||||
<% end %>
|
||||
|
|
|
@ -1,3 +1,83 @@
|
|||
<% entry = @entry %>
|
||||
<% entry, account = @entry, @entry.account %>
|
||||
|
||||
<%= render "account/valuations/valuation", entry: entry %>
|
||||
<%= drawer do %>
|
||||
<header class="mb-4 space-y-1">
|
||||
<span class="text-gray-500 text-sm">
|
||||
<%= t(".balance") %>
|
||||
</span>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<h3 class="font-medium">
|
||||
<span class="text-2xl">
|
||||
<%= format_money entry.amount_money %>
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<span class="text-sm text-gray-500">
|
||||
<%= I18n.l(entry.date, format: :long) %>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div class="space-y-2">
|
||||
<!-- Overview Section -->
|
||||
<%= disclosure t(".overview") do %>
|
||||
<div class="pb-4">
|
||||
<%= styled_form_with model: [account, entry],
|
||||
url: account_entry_path(account, entry),
|
||||
class: "space-y-2",
|
||||
data: { controller: "auto-submit-form" } do |f| %>
|
||||
<%= f.text_field :name,
|
||||
label: t(".name_label"),
|
||||
placeholder: t(".name_placeholder"),
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
|
||||
<%= f.date_field :date,
|
||||
label: t(".date_label"),
|
||||
max: Date.current,
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
|
||||
<%= f.money_field :amount,
|
||||
label: t(".amount"),
|
||||
auto_submit: true,
|
||||
disable_currency: true %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Details Section -->
|
||||
<%= disclosure t(".details") do %>
|
||||
<div class="pb-4">
|
||||
<%= styled_form_with model: [account, entry],
|
||||
url: account_entry_path(account, entry),
|
||||
class: "space-y-2",
|
||||
data: { controller: "auto-submit-form" } do |f| %>
|
||||
<%= f.text_area :notes,
|
||||
label: t(".note_label"),
|
||||
placeholder: t(".note_placeholder"),
|
||||
rows: 5,
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Settings Section -->
|
||||
<%= disclosure t(".settings") do %>
|
||||
<div class="pb-4">
|
||||
<!-- Delete Valuation Form -->
|
||||
<div class="flex items-center justify-between gap-2 p-3">
|
||||
<div class="text-sm space-y-1">
|
||||
<h4 class="text-gray-900"><%= t(".delete_title") %></h4>
|
||||
<p class="text-gray-500"><%= t(".delete_subtitle") %></p>
|
||||
</div>
|
||||
|
||||
<%= button_to t(".delete"),
|
||||
account_entry_path(account, entry),
|
||||
method: :delete,
|
||||
class: "rounded-lg px-3 py-2 text-red-500 text-sm font-medium border border-alpha-black-200",
|
||||
data: { turbo_confirm: true, turbo_frame: "_top" } %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
<%# locals: (account:, return_to: nil) %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(account) do %>
|
||||
<div class="p-4 flex items-center justify-between gap-3 group/account">
|
||||
<div class="flex items-center gap-3">
|
||||
|
@ -16,7 +18,7 @@
|
|||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= link_to edit_account_path(account), data: { turbo_frame: :modal }, class: "group-hover/account:flex hidden hover:opacity-80 items-center justify-center" do %>
|
||||
<%= link_to edit_account_path(account, return_to: return_to), 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>
|
||||
|
|
|
@ -1,35 +1,37 @@
|
|||
<%# locals: (group:) -%>
|
||||
<% type = Accountable.from_type(group.name) %>
|
||||
<% if group && group.children.any? %>
|
||||
<details class="mb-1 text-sm group" data-controller="account-collapse" data-account-collapse-type-value="<%= type %>">
|
||||
<summary class="flex gap-4 px-3 py-2 items-center w-full rounded-[10px] font-medium hover:bg-gray-100 cursor-pointer">
|
||||
<%= lucide_icon("chevron-down", class: "hidden group-open:block text-gray-500 w-5 h-5") %>
|
||||
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
<% group_trend = group.series.trend %>
|
||||
|
||||
<details
|
||||
class="mb-1 text-sm group"
|
||||
data-controller="account-collapse"
|
||||
data-account-collapse-type-value="<%= type %>">
|
||||
<summary class="flex gap-4 px-3 py-2 items-center w-full rounded-[10px] font-medium
|
||||
hover:bg-gray-100 cursor-pointer">
|
||||
<%= lucide_icon("chevron-down",
|
||||
class: "hidden group-open:block text-gray-500 w-5 h-5") %>
|
||||
<%= lucide_icon("chevron-right",
|
||||
class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
|
||||
<div class="text-left"><%= type.model_name.human %></div>
|
||||
|
||||
<div class="ml-auto flex flex-col items-end">
|
||||
<p class="text-right"><%= format_money group.sum %></p>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<%=
|
||||
tag.div(
|
||||
id: "#{group.name}_sparkline",
|
||||
class: "h-3 w-8 ml-auto",
|
||||
data: {
|
||||
controller: "time-series-chart",
|
||||
"time-series-chart-data-value": group.series.to_json,
|
||||
"time-series-chart-stroke-width-value": 1,
|
||||
"time-series-chart-use-labels-value": false,
|
||||
"time-series-chart-use-tooltip-value": false
|
||||
}
|
||||
)
|
||||
%>
|
||||
<% styles = trend_styles(group.series.trend) %>
|
||||
<span class="text-xs <%= styles[:text_class] %>"><%= sprintf("%+.2f", group.series.trend.percent) %>%</span>
|
||||
<div class="h-3 w-8">
|
||||
<%= render "shared/sparkline", series: group.series, id: "#{group.name}_sparkline" %>
|
||||
</div>
|
||||
|
||||
<span class="text-xs" style="color: <%= group_trend.color %>"><%= group_trend.value.positive? ? "+" : "" %><%= group_trend.percent.infinite? ? "∞" : number_to_percentage(group_trend.percent, precision: 0) %></span>
|
||||
</div>
|
||||
</div>
|
||||
</summary>
|
||||
<% 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 %>
|
||||
<% account_trend = account_value_node.series.trend %>
|
||||
<%= link_to account, class: "flex items-center w-full gap-3 px-3 py-2 mb-1 hover:bg-gray-100 rounded-[10px]" do %>
|
||||
<%= render "accounts/logo", account: account, size: "sm" %>
|
||||
<div>
|
||||
<p class="font-medium"><%= account_value_node.name %></p>
|
||||
|
@ -37,31 +39,21 @@
|
|||
<p class="text-xs text-gray-500"><%= account.subtype&.humanize %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex flex-col ml-auto font-medium text-right">
|
||||
<div class="flex flex-col items-end font-medium text-right ml-auto">
|
||||
<p><%= format_money account.balance_money %></p>
|
||||
<% unless account_value_node.series.trend.direction.flat? %>
|
||||
<div class="flex items-center gap-1">
|
||||
<%=
|
||||
tag.div(
|
||||
id: dom_id(account, :list_sparkline),
|
||||
class: "h-3 w-8 ml-auto",
|
||||
data: {
|
||||
controller: "time-series-chart",
|
||||
"time-series-chart-data-value": account_value_node.series.to_json,
|
||||
"time-series-chart-stroke-width-value": 1,
|
||||
"time-series-chart-use-labels-value": false,
|
||||
"time-series-chart-use-tooltip-value": false
|
||||
}
|
||||
)
|
||||
%>
|
||||
<% styles = trend_styles(account_value_node.series.trend) %>
|
||||
<span class="text-xs <%= styles[:text_class] %>"><%= sprintf("%+.2f", account_value_node.series.trend.percent) %>%</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="h-3 w-8">
|
||||
<%= render "shared/sparkline", series: account_value_node.series, id: dom_id(account, :list_sparkline) %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<span class="text-xs" style="color: <%= account_trend.color %>">
|
||||
<%= account_trend.value.positive? ? "+" : "" %><%= account_trend.percent.infinite? ? "∞" : number_to_percentage(account_trend.percent, precision: 0) %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% 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 %>
|
||||
<%= link_to new_polymorphic_path(type, step: "method_select"), 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") %>
|
||||
<%= t(".new_account", type: type.model_name.human.downcase) %>
|
||||
<% end %>
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
<%= link_to new_account_path(
|
||||
type: type.class.name.demodulize,
|
||||
institution_id: params[:institution_id]
|
||||
),
|
||||
<%# locals: (accountable:) %>
|
||||
|
||||
<%= link_to new_polymorphic_path(accountable, institution_id: params[:institution_id], step: "method_select", return_to: params[:return_to]),
|
||||
class: "flex items-center gap-4 w-full text-center focus:outline-none focus:bg-alpha-black-25 hover:bg-alpha-black-25 border border-transparent block px-2 rounded-lg p-2" do %>
|
||||
<span class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg <%= bg_color %> border border-alpha-black-25">
|
||||
<%= lucide_icon(icon, class: "#{text_color} w-5 h-5") %>
|
||||
<span style="background-color: color-mix(in srgb, <%= accountable.color %> 10%, white);" class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg border border-alpha-black-25">
|
||||
<%= lucide_icon(accountable.icon, style: "color: #{accountable.color}", class: "w-5 h-5") %>
|
||||
</span>
|
||||
<%= type.model_name.human %>
|
||||
<%= accountable.model_name.human %>
|
||||
<% end %>
|
||||
|
|
|
@ -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(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 %>
|
||||
<%= 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 %>
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
<%# 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(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>
|
||||
<%= text %>
|
||||
<% end %>
|
||||
<% end %>
|
|
@ -1,28 +1,21 @@
|
|||
<%# locals: (account:, url:) %>
|
||||
|
||||
<%= styled_form_with model: account, url: url, scope: :account, class: "flex flex-col gap-4 justify-between grow", data: { turbo: false } do |f| %>
|
||||
<%= styled_form_with model: account, url: url, scope: :account, data: { turbo: false }, class: "flex flex-col gap-4 justify-between grow" do |form| %>
|
||||
<div class="grow space-y-2">
|
||||
<% unless account.new_record? %>
|
||||
<% if account.accountable.mode_required? %>
|
||||
<%= f.select :mode, Account::VALUE_MODES.map { |mode| [mode.titleize, mode] }, { label: t(".mode"), prompt: t(".mode_prompt") }, required: true %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= 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") %>
|
||||
<%= form.hidden_field :accountable_type %>
|
||||
<%= form.hidden_field :return_to, value: params[:return_to] %>
|
||||
|
||||
<% if account.new_record? %>
|
||||
<%= f.hidden_field :institution_id %>
|
||||
<%= form.hidden_field :institution_id %>
|
||||
<% else %>
|
||||
<%= f.collection_select :institution_id, Current.family.institutions.alphabetically, :id, :name, { include_blank: t(".ungrouped"), label: t(".institution") } %>
|
||||
<%= form.collection_select :institution_id, Current.family.institutions.alphabetically, :id, :name, { include_blank: t(".ungrouped"), label: t(".institution") } %>
|
||||
<% end %>
|
||||
|
||||
<%= f.money_field :balance, label: t(".balance"), required: true, default_currency: Current.family.currency %>
|
||||
<%= form.text_field :name, placeholder: t(".name_placeholder"), required: "required", label: t(".name_label") %>
|
||||
<%= form.money_field :balance, label: t(".balance"), required: true, default_currency: Current.family.currency %>
|
||||
|
||||
<% if account.accountable %>
|
||||
<%= render permitted_accountable_partial(account, "form"), f: f %>
|
||||
<% end %>
|
||||
<%= yield form %>
|
||||
</div>
|
||||
|
||||
<%= f.submit %>
|
||||
<%= form.submit %>
|
||||
<% end %>
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<%# locals: (account:) %>
|
||||
|
||||
<%= render partial: "accounts/accountables/#{account.accountable_type.underscore}/overview", locals: { account: account } %>
|
|
@ -1,7 +0,0 @@
|
|||
<div class="flex items-center gap-3">
|
||||
<%= render "accounts/logo", account: account %>
|
||||
|
||||
<div>
|
||||
<h2 class="font-medium text-xl"><%= account.name %></h2>
|
||||
</div>
|
||||
</div>
|
|
@ -1,13 +0,0 @@
|
|||
<%# locals: (account:, selected_tab:) %>
|
||||
|
||||
<% if account.mode.nil? %>
|
||||
<%= render "accounts/accountables/value_onboarding", account: account %>
|
||||
<% else %>
|
||||
<div class="min-h-[800px]">
|
||||
<% if account.mode == "transactions" %>
|
||||
<%= render "accounts/accountables/transactions", account: account %>
|
||||
<% else %>
|
||||
<%= render "accounts/accountables/valuations", account: account %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
|
@ -1,5 +0,0 @@
|
|||
<%# locals: (account:) %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(account, :transactions), src: account_transactions_path(account) do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% end %>
|
|
@ -1,5 +0,0 @@
|
|||
<%# locals: (account:) %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(account, :valuations), src: account_valuations_path(account) do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% end %>
|
|
@ -1,16 +0,0 @@
|
|||
<%# locals: (account:) %>
|
||||
|
||||
<div data-test-id="value-onboarding" class="py-12 flex flex-col justify-center items-center bg-white rounded-lg border border-alpha-black-25 shadow-xs">
|
||||
<h3 class="font-medium text-lg mb-2">How would you like to track value for this account?</h3>
|
||||
<p class="text-sm text-gray-500 mb-8">We will use this to determine what data to show for this account.</p>
|
||||
<div class="flex items-center gap-4">
|
||||
<%= button_to account_path(account, { account: { mode: "balance" } }), method: :put, class: "btn btn--outline", data: { controller: "tooltip", turbo: false } do %>
|
||||
<%= render partial: "shared/text_tooltip", locals: { tooltip_text: "Choose this if you only need to track the historical value of this account over time and do not plan on importing any transactions." } %>
|
||||
<span>Balance only</span>
|
||||
<% end %>
|
||||
<%= button_to account_path(account, { account: { mode: "transactions" } }), method: :put, class: "btn btn--primary", data: { controller: "tooltip", turbo: false } do %>
|
||||
<%= render partial: "shared/text_tooltip", locals: { tooltip_text: "Choose this if you plan on importing transactions into this account for budgeting and other analytics." } %>
|
||||
<span>Transactions</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
|
@ -1,21 +0,0 @@
|
|||
<div>
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="space-y-2">
|
||||
<%= f.fields_for :accountable do |credit_card_form| %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= credit_card_form.number_field :available_credit, label: t(".available_credit"), placeholder: t(".available_credit_placeholder"), min: 0 %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<%= credit_card_form.number_field :minimum_payment, label: t(".minimum_payment"), placeholder: t(".minimum_payment_placeholder"), min: 0 %>
|
||||
<%= credit_card_form.number_field :apr, label: t(".apr"), placeholder: t(".apr_placeholder"), min: 0, step: 0.01 %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<%= credit_card_form.date_field :expiration_date, label: t(".expiration_date") %>
|
||||
<%= credit_card_form.number_field :annual_fee, label: t(".annual_fee"), placeholder: t(".annual_fee_placeholder"), min: 0 %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
|
@ -1 +0,0 @@
|
|||
<%= render "accounts/accountables/default_header", account: account %>
|
|
@ -1,26 +0,0 @@
|
|||
<%# locals: (account:, selected_tab:) %>
|
||||
|
||||
<% if account.mode.nil? %>
|
||||
<%= render "accounts/accountables/value_onboarding", account: account %>
|
||||
<% else %>
|
||||
<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"]) %>
|
||||
|
||||
<% if account.mode == "transactions" %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "transactions", is_selected: selected_tab == "transactions" %>
|
||||
<% else %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "value", is_selected: selected_tab == "value" %>
|
||||
<% end %>
|
||||
</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 %>
|
||||
<% when "value" %>
|
||||
<%= render "accounts/accountables/valuations", account: account %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
|
@ -1 +0,0 @@
|
|||
<%= render "accounts/accountables/default_header", account: account %>
|
|
@ -1 +0,0 @@
|
|||
<%= render "accounts/accountables/default_tabs", account: account, selected_tab: selected_tab %>
|
|
@ -1 +0,0 @@
|
|||
<%= f.select :subtype, [["Checking", "checking"], ["Savings", "savings"]], { label: true, prompt: t(".prompt"), include_blank: t(".none") } %>
|
|
@ -1 +0,0 @@
|
|||
<%= render "accounts/accountables/default_header", account: account %>
|
|
@ -1 +0,0 @@
|
|||
<%= render "accounts/accountables/default_tabs", account: account, selected_tab: selected_tab %>
|
|
@ -1 +0,0 @@
|
|||
<%= f.select :subtype, Investment::SUBTYPES, { label: true, prompt: t(".prompt"), include_blank: t(".none") } %>
|
|
@ -1 +0,0 @@
|
|||
<%= render "accounts/accountables/default_header", account: account %>
|
|
@ -1,34 +0,0 @@
|
|||
<%# locals: (account:, selected_tab:) %>
|
||||
|
||||
<% if account.mode.nil? %>
|
||||
<%= render "accounts/accountables/value_onboarding", account: account %>
|
||||
<% else %>
|
||||
<div class="flex gap-2 text-sm text-gray-900 font-medium mb-4">
|
||||
<% if account.mode == "transactions" %>
|
||||
<%= 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" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="min-h-[800px]">
|
||||
<% if account.mode == "transactions" %>
|
||||
<% 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 %>
|
||||
<% else %>
|
||||
<%= render "accounts/accountables/valuations", account: account %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
|
@ -1,16 +0,0 @@
|
|||
<div>
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="space-y-2">
|
||||
<%= f.fields_for :accountable do |loan_form| %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= loan_form.number_field :interest_rate, label: t(".interest_rate"), placeholder: t(".interest_rate_placeholder"), min: 0, step: 0.01 %>
|
||||
<%= loan_form.select :rate_type, options_for_select([["Fixed", "fixed"], ["Variable", "variable"], ["Adjustable", "adjustable"]]), { label: t(".rate_type") } %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<%= loan_form.number_field :term_months, label: t(".term_months"), placeholder: t(".term_months_placeholder") %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
|
@ -1 +0,0 @@
|
|||
<%= render "accounts/accountables/default_header", account: account %>
|
|
@ -1,25 +0,0 @@
|
|||
<%# locals: (account:, selected_tab:) %>
|
||||
|
||||
<% if account.mode.nil? %>
|
||||
<%= render "accounts/accountables/value_onboarding", account: account %>
|
||||
<% else %>
|
||||
<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"]) %>
|
||||
<% if account.mode == "transactions" %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "transactions", is_selected: selected_tab == "transactions" %>
|
||||
<% else %>
|
||||
<%= render "accounts/accountables/tab", account: account, key: "value", is_selected: selected_tab == "value" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="min-h-[800px]">
|
||||
<% case selected_tab %>
|
||||
<% when nil, "overview" %>
|
||||
<%= render "accounts/accountables/loan/overview", account: account %>
|
||||
<% when "transactions" %>
|
||||
<%= render "accounts/accountables/transactions", account: account %>
|
||||
<% when "value" %>
|
||||
<%= render "accounts/accountables/valuations", account: account %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
|
@ -1 +0,0 @@
|
|||
<%= render "accounts/accountables/default_header", account: account %>
|
|
@ -1 +0,0 @@
|
|||
<%= render "accounts/accountables/valuations", account: account %>
|
|
@ -1 +0,0 @@
|
|||
<%= render "accounts/accountables/default_header", account: account %>
|
|
@ -1 +0,0 @@
|
|||
<%= render "accounts/accountables/valuations", account: account %>
|
|
@ -1,36 +0,0 @@
|
|||
<%# locals: (f:) %>
|
||||
|
||||
<div>
|
||||
<hr class="my-4">
|
||||
|
||||
<h3 class="my-4 font-medium"><%= t(".additional_info") %> (<%= t(".optional") %>)</h3>
|
||||
|
||||
<div class="space-y-2">
|
||||
<%= f.fields_for :accountable do |af| %>
|
||||
<div class="flex gap-2">
|
||||
<%= af.number_field :year_built, label: t(".year_built"), placeholder: 2005, min: 1700, max: Time.current.year %>
|
||||
<%= af.number_field :area_value, label: t(".area_value"), placeholder: 2000, min: 1 %>
|
||||
<%= af.select :area_unit,
|
||||
[["Square feet", "sqft"], ["Square meters", "sqm"]],
|
||||
{ label: t(".area_unit") } %>
|
||||
</div>
|
||||
|
||||
<%= af.fields_for :address do |address_form| %>
|
||||
<div class="flex gap-2">
|
||||
<%= address_form.text_field :line1, label: t(".line1"), placeholder: "123 Main St" %>
|
||||
<%= address_form.text_field :line2, label: t(".line2"), placeholder: "Apt 1" %>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<%= address_form.text_field :locality, label: t(".city"), placeholder: "Sacramento" %>
|
||||
<%= address_form.text_field :region, label: t(".state"), placeholder: "CA" %>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<%= address_form.text_field :postal_code, label: t(".postal_code"), placeholder: "95814" %>
|
||||
<%= address_form.text_field :country, label: t(".country"), placeholder: "USA" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
|
@ -1,11 +0,0 @@
|
|||
<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>
|
|
@ -1,15 +0,0 @@
|
|||
<%# 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,23 +0,0 @@
|
|||
<%# locals: (f:) %>
|
||||
|
||||
<div>
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="space-y-2">
|
||||
<%= f.fields_for :accountable do |vehicle_form| %>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<%= vehicle_form.text_field :make, label: t(".make"), placeholder: t(".make_placeholder") %>
|
||||
<%= vehicle_form.text_field :model, label: t(".model"), placeholder: t(".model_placeholder") %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<%= vehicle_form.number_field :year, label: t(".year"), placeholder: t(".year_placeholder"), min: 1900, max: Time.current.year %>
|
||||
<%= vehicle_form.number_field :mileage_value, label: t(".mileage"), placeholder: t(".mileage_placeholder"), min: 0 %>
|
||||
<%= vehicle_form.select :mileage_unit,
|
||||
[["Miles", "mi"], ["Kilometers", "km"]],
|
||||
{ label: t(".mileage_unit") } %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
|
@ -1 +0,0 @@
|
|||
<%= render "accounts/accountables/default_header", account: account %>
|
|
@ -1,15 +0,0 @@
|
|||
<%# 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(step: "method"),
|
||||
<%= link_to new_account_path(return_to: accounts_path),
|
||||
data: { turbo_frame: "modal" },
|
||||
class: "btn btn--primary flex items-center gap-1" do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
|
@ -35,11 +35,11 @@
|
|||
<% else %>
|
||||
<div class="space-y-2">
|
||||
<% @institutions.each do |institution| %>
|
||||
<%= render "institution_accounts", institution: %>
|
||||
<%= render "accounts/index/institution_accounts", institution: %>
|
||||
<% end %>
|
||||
|
||||
<% if @accounts.any? %>
|
||||
<%= render "institutionless_accounts", accounts: @accounts %>
|
||||
<%= render "accounts/index/institutionless_accounts", accounts: @accounts %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<%= 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),
|
||||
<%= link_to new_account_path(institution_id: institution.id, return_to: accounts_path),
|
||||
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" %>
|
||||
|
@ -81,7 +81,7 @@
|
|||
<% 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 %>
|
||||
<%= link_to new_account_path(institution_id: institution.id, return_to: accounts_path), 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 %>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue