mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-02 20:15:22 +02:00
Multi-currency support: Money + Currency class improvements (#553)
* Money improvements * Replace all old money usage
This commit is contained in:
parent
e5750d1a13
commit
fe2fa0eac1
43 changed files with 2982 additions and 196 deletions
|
@ -42,7 +42,7 @@
|
|||
}
|
||||
|
||||
.form-field__input {
|
||||
@apply p-3 pt-1 w-full bg-transparent border-none opacity-50;
|
||||
@apply p-3 w-full bg-transparent border-none opacity-50;
|
||||
@apply focus:outline-none focus:ring-0 focus:opacity-100;
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ class TransactionsController < ApplicationController
|
|||
|
||||
respond_to do |format|
|
||||
if @transaction.save
|
||||
format.html { redirect_to transaction_url(@transaction), notice: t(".success") }
|
||||
format.html { redirect_to transactions_url, notice: t(".success") }
|
||||
else
|
||||
format.html { render :new, status: :unprocessable_entity }
|
||||
end
|
||||
|
|
|
@ -22,6 +22,34 @@ class ApplicationFormBuilder < ActionView::Helpers::FormBuilder
|
|||
RUBY_EVAL
|
||||
end
|
||||
|
||||
# See `Monetizable` concern, which adds a _money suffix to the attribute name
|
||||
# For a monetized field, the setter will always be the attribute name without the _money suffix
|
||||
def money_field(method, options = {})
|
||||
money = @object.send(method)
|
||||
raise ArgumentError, "The value of #{method} is not a Money object" unless money.is_a?(Money) || money.nil?
|
||||
|
||||
money_amount_method = method.to_s.chomp("_money").to_sym
|
||||
money_currency_method = :currency
|
||||
|
||||
readonly_currency = options[:readonly_currency] || false
|
||||
|
||||
default_options = {
|
||||
class: "form-field__input",
|
||||
value: money&.amount,
|
||||
placeholder: Money.new(0, money&.currency || Money.default_currency).format
|
||||
}
|
||||
|
||||
merged_options = default_options.merge(options)
|
||||
|
||||
@template.form_field_tag do
|
||||
(label(method, *label_args(options)).to_s if options[:label]) +
|
||||
@template.tag.div(class: "flex items-center") do
|
||||
number_field(money_amount_method, merged_options.except(:label)) +
|
||||
select(money_currency_method, Money::Currency.popular.map(&:iso_code), { selected: money&.currency&.iso_code }, { disabled: readonly_currency, class: "ml-auto form-field__input w-fit pr-8" })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def select(method, choices, options = {}, html_options = {})
|
||||
default_options = { class: "form-field__input" }
|
||||
merged_options = default_options.merge(html_options)
|
||||
|
|
|
@ -27,9 +27,7 @@ module ApplicationHelper
|
|||
render partial: "shared/modal", locals: { content: content }
|
||||
end
|
||||
|
||||
def currency_dropdown(f: nil, options: [])
|
||||
render partial: "shared/currency_dropdown", locals: { f: f, options: options }
|
||||
end
|
||||
|
||||
|
||||
def sidebar_modal(&block)
|
||||
content = capture &block
|
||||
|
@ -97,12 +95,15 @@ module ApplicationHelper
|
|||
end
|
||||
end
|
||||
|
||||
def format_currency(number, options = {})
|
||||
user_currency_preference = Current.family.try(:currency) || "USD"
|
||||
def format_money(number_or_money, options = {})
|
||||
money = Money.new(number_or_money)
|
||||
options.reverse_merge!(money.default_format_options)
|
||||
number_to_currency(money.amount, options)
|
||||
end
|
||||
|
||||
currency_options = CURRENCY_OPTIONS[user_currency_preference.to_sym]
|
||||
options.reverse_merge!(currency_options)
|
||||
|
||||
number_to_currency(number, options)
|
||||
def format_money_without_symbol(number_or_money, options = {})
|
||||
money = Money.new(number_or_money)
|
||||
options.reverse_merge!(money.default_format_options)
|
||||
ActiveSupport::NumberHelper.number_to_delimited(money.amount.round(options[:precision] || 0), { delimiter: options[:delimiter], separator: options[:separator] })
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,4 +2,8 @@ module FormsHelper
|
|||
def form_field_tag(&)
|
||||
tag.div class: "form-field", &
|
||||
end
|
||||
|
||||
def currency_dropdown(f: nil, options: [])
|
||||
render partial: "shared/currency_dropdown", locals: { f: f, options: options }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,7 +17,7 @@ export default class extends Controller {
|
|||
|
||||
renderChart = () => {
|
||||
this.drawChart(this.seriesValue);
|
||||
}
|
||||
};
|
||||
|
||||
trendStyles(trendDirection) {
|
||||
return {
|
||||
|
@ -45,11 +45,11 @@ export default class extends Controller {
|
|||
formatted: {
|
||||
value: Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: b.currency || "USD",
|
||||
currency: b.currency.iso_code || "USD",
|
||||
}).format(b.amount),
|
||||
change: Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: b.currency || "USD",
|
||||
currency: b.currency.iso_code || "USD",
|
||||
signDisplay: "always",
|
||||
}).format(b.trend.amount),
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
class Account < ApplicationRecord
|
||||
include Syncable
|
||||
include Monetizable
|
||||
|
||||
validates :family, presence: true
|
||||
|
||||
|
@ -9,6 +10,8 @@ class Account < ApplicationRecord
|
|||
has_many :valuations
|
||||
has_many :transactions
|
||||
|
||||
monetize :balance
|
||||
|
||||
enum :status, { ok: "ok", syncing: "syncing", error: "error" }, validate: true
|
||||
|
||||
scope :active, -> { where(is_active: true) }
|
||||
|
|
|
@ -6,8 +6,8 @@ class Account::BalanceCalculator
|
|||
def daily_balances(start_date = nil)
|
||||
calc_start_date = [ start_date, @account.effective_start_date ].compact.max
|
||||
|
||||
valuations = @account.valuations.where("date >= ?", calc_start_date).order(:date).select(:date, :value)
|
||||
transactions = @account.transactions.where("date > ?", calc_start_date).order(:date).select(:date, :amount)
|
||||
valuations = @account.valuations.where("date >= ?", calc_start_date).order(:date).select(:date, :value, :currency)
|
||||
transactions = @account.transactions.where("date > ?", calc_start_date).order(:date).select(:date, :amount, :currency)
|
||||
oldest_entry = [ valuations.first, transactions.first ].compact.min_by(&:date)
|
||||
|
||||
net_transaction_flows = transactions.sum(&:amount)
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
class AccountBalance < ApplicationRecord
|
||||
belongs_to :account
|
||||
include Monetizable
|
||||
|
||||
belongs_to :account
|
||||
validates :account, :date, :balance, presence: true
|
||||
monetize :balance
|
||||
|
||||
scope :in_period, ->(period) { period.date_range.nil? ? all : where(date: period.date_range) }
|
||||
|
||||
|
|
14
app/models/concerns/monetizable.rb
Normal file
14
app/models/concerns/monetizable.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
module Monetizable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
def monetize(*fields)
|
||||
fields.each do |field|
|
||||
define_method("#{field}_money") do
|
||||
value = self.send(field)
|
||||
value.nil? ? nil : Money.new(value, currency)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,9 +1,13 @@
|
|||
class Family < ApplicationRecord
|
||||
include Monetizable
|
||||
|
||||
has_many :users, dependent: :destroy
|
||||
has_many :accounts, dependent: :destroy
|
||||
has_many :transactions, through: :accounts
|
||||
has_many :transaction_categories, dependent: :destroy, class_name: "Transaction::Category"
|
||||
|
||||
monetize :net_worth, :assets, :liabilities
|
||||
|
||||
def snapshot(period = Period.all)
|
||||
query = accounts.active.joins(:balances)
|
||||
.where("account_balances.currency = ?", self.currency)
|
||||
|
@ -35,7 +39,7 @@ class Family < ApplicationRecord
|
|||
end
|
||||
|
||||
def assets
|
||||
accounts.active.assets.sum(:balance)
|
||||
accounts.active.assets.sum(:balance)
|
||||
end
|
||||
|
||||
def liabilities
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
class Money
|
||||
attr_reader :amount, :currency
|
||||
|
||||
def self.from_amount(amount, currency = "USD")
|
||||
Money.new(amount, currency)
|
||||
end
|
||||
|
||||
def initialize(amount, currency = :USD)
|
||||
@amount = amount
|
||||
@currency = currency
|
||||
end
|
||||
|
||||
def cents(precision: nil)
|
||||
_precision = precision || CURRENCY_OPTIONS[@currency.to_sym][:precision]
|
||||
return "" unless _precision.positive?
|
||||
|
||||
fractional_part = @amount.to_s.split(".")[1] || ""
|
||||
fractional_part = fractional_part[0, _precision].ljust(_precision, "0")
|
||||
end
|
||||
|
||||
def symbol
|
||||
CURRENCY_OPTIONS[@currency.to_sym][:symbol]
|
||||
end
|
||||
|
||||
def separator
|
||||
CURRENCY_OPTIONS[@currency.to_sym][:separator]
|
||||
end
|
||||
|
||||
def precision
|
||||
CURRENCY_OPTIONS[@currency.to_sym][:precision]
|
||||
end
|
||||
end
|
|
@ -14,7 +14,7 @@ class MoneySeries
|
|||
{
|
||||
raw: current,
|
||||
date: current.date,
|
||||
value: Money.from_amount(current.send(@accessor), current.currency),
|
||||
value: Money.new(current.send(@accessor), current.currency),
|
||||
trend: Trend.new(
|
||||
current: current.send(@accessor),
|
||||
previous: previous&.send(@accessor),
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
class Transaction < ApplicationRecord
|
||||
include Monetizable
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :category, optional: true
|
||||
|
||||
|
@ -6,6 +8,12 @@ class Transaction < ApplicationRecord
|
|||
|
||||
after_commit :sync_account
|
||||
|
||||
monetize :amount
|
||||
|
||||
scope :inflows, -> { where("amount > 0") }
|
||||
scope :outflows, -> { where("amount < 0") }
|
||||
scope :active, -> { where(excluded: false) }
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
%w[name amount date]
|
||||
end
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
class Valuation < ApplicationRecord
|
||||
include Monetizable
|
||||
|
||||
belongs_to :account
|
||||
|
||||
validates :account, :date, :value, presence: true
|
||||
|
||||
after_commit :sync_account
|
||||
monetize :value
|
||||
|
||||
scope :in_period, ->(period) { period.date_range.nil? ? all : where(date: period.date_range) }
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
</div>
|
||||
<div class="flex items-center gap-8">
|
||||
<p class="text-sm font-medium <%= account.is_active ? "text-gray-900" : "text-gray-400" %>">
|
||||
<%= format_currency account.balance %>
|
||||
<%= format_money account.balance_money %>
|
||||
</p>
|
||||
<%= form_with model: account, method: :patch, html: { class: "flex items-center", data: { turbo_frame: "_top" } } do |form| %>
|
||||
<div class="relative inline-block select-none">
|
||||
|
|
|
@ -1,31 +1,27 @@
|
|||
<%# locals: (type:) -%>
|
||||
|
||||
<% accounts = Current.family.accounts.where(accountable_type: type.name) %>
|
||||
|
||||
<% if accounts.sum(&:converted_balance) > 0 %>
|
||||
<details class="mb-1 text-sm group" data-controller="account-collapse" data-account-collapse-type-value="<%= type %>">
|
||||
<summary class="flex gap-4 px-2 py-3 items-center w-full rounded-[10px] font-medium hover:bg-gray-100">
|
||||
<%= 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"><%= format_currency accounts.sum(&:converted_balance) %></div>
|
||||
</summary>
|
||||
|
||||
<% accounts.each do |account| %>
|
||||
<%= link_to account_path(account), class: "flex items-center w-full gap-3 px-2 py-3 mb-1 hover:bg-gray-100 rounded-[10px]" do %>
|
||||
<div>
|
||||
<p class="font-medium"><%= account.name %></p>
|
||||
<% if account.subtype %>
|
||||
<p class="text-xs text-gray-500"><%= account.subtype&.humanize %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="ml-auto font-medium"><%= format_currency account.converted_balance %></p>
|
||||
<details class="mb-1 text-sm group" data-controller="account-collapse" data-account-collapse-type-value="<%= type %>">
|
||||
<summary class="flex gap-4 px-2 py-3 items-center w-full rounded-[10px] font-medium hover:bg-gray-100">
|
||||
<%= 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"><%= format_money accounts.sum(&:converted_balance) %></div>
|
||||
</summary>
|
||||
<% accounts.each do |account| %>
|
||||
<%= link_to account_path(account), class: "flex items-center w-full gap-3 px-2 py-3 mb-1 hover:bg-gray-100 rounded-[10px]" do %>
|
||||
<div>
|
||||
<p class="font-medium"><%= account.name %></p>
|
||||
<% if account.subtype %>
|
||||
<p class="text-xs text-gray-500"><%= account.subtype&.humanize %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="ml-auto font-medium"><%= format_money account.converted_balance %></p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_account_path(step: 'method', type: type.name.demodulize), class: "flex items-center gap-4 px-2 py-3 mb-1 text-gray-500 text-sm font-medium rounded-[10px] hover:bg-gray-100", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<p>New <%= type.model_name.human.downcase %></p>
|
||||
<% end %>
|
||||
</details>
|
||||
<%= link_to new_account_path(step: 'method', type: type.name.demodulize), class: "flex items-center gap-4 px-2 py-3 mb-1 text-gray-500 text-sm font-medium rounded-[10px] hover:bg-gray-100", data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<p>New <%= type.model_name.human.downcase %></p>
|
||||
<% end %>
|
||||
</details>
|
||||
<% end %>
|
||||
|
|
|
@ -15,13 +15,13 @@
|
|||
<%# TODO: Add descriptive name of valuation %>
|
||||
<p class="text-gray-500">Manually entered</p>
|
||||
</div>
|
||||
<div class="flex text-sm font-medium text-right"><%= format_currency(valuation.value) %></div>
|
||||
<div class="flex text-sm font-medium text-right"><%= format_money valuation.value_money %></div>
|
||||
</div>
|
||||
<div class="flex w-56 justify-end text-right text-sm font-medium">
|
||||
<% if trend.amount == 0 %>
|
||||
<span class="text-gray-500">No change</span>
|
||||
<% else %>
|
||||
<span class="<%= valuation_styles[:text_class] %>"><%= valuation_styles[:symbol] %><%= format_currency(trend.amount.abs) %></span>
|
||||
<span class="<%= valuation_styles[:text_class] %>"><%= valuation_styles[:symbol] %><%= format_money trend.amount.abs %></span>
|
||||
<span class="<%= valuation_styles[:text_class] %>">(<%= lucide_icon(valuation_styles[:icon], class: "w-4 h-4 align-text-bottom inline") %> <%= trend.percent %>%)</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
<p><%= to_accountable_title(Accountable.from_type(group)) %></p>
|
||||
<span class="text-gray-400 mx-2">·</span>
|
||||
<p><%= accounts.count %></p>
|
||||
<p class="ml-auto"><%= format_currency accounts.sum(&:balance) %></p>
|
||||
<p class="ml-auto"><%= format_money accounts.sum(&:balance_money) %></p>
|
||||
</div>
|
||||
<div class="bg-white">
|
||||
<% accounts.each do |account| %>
|
||||
|
|
|
@ -69,11 +69,7 @@
|
|||
<%= f.hidden_field :accountable_type %>
|
||||
<%= f.text_field :name, placeholder: t('accounts.new.name.placeholder'), required: 'required', label: t('accounts.new.name.label'), autofocus: true %>
|
||||
<%= render "accounts/#{permitted_accountable_partial(@account.accountable_type)}", f: f %>
|
||||
<%= form_field_tag do %>
|
||||
<%= f.label :balance, class: "form-field__label" %>
|
||||
<%= f.number_field :balance, step: :any, placeholder: number_to_currency(0), in: 0.00..100000000.00, required: 'required', class: 'form-field__input max-w-[80%]' %>
|
||||
<%= currency_dropdown(f: f, options: Currency.all.order(:iso_code).pluck(:iso_code)) if Currency.count > 1 %>
|
||||
<% end %>
|
||||
<%= f.money_field :balance_money, label: "Balance", required: 'required' %>
|
||||
</div>
|
||||
<%= f.submit "Add #{@account.accountable.model_name.human.downcase}" %>
|
||||
<% end %>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<%= turbo_stream_from @account %>
|
||||
<% balance = Money.from_amount(@account.balance, @account.currency) %>
|
||||
<% balance = Money.new(@account.balance, @account.currency) %>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
|
@ -11,7 +11,7 @@
|
|||
<div class="flex items-center gap-3">
|
||||
<div class="relative cursor-not-allowed">
|
||||
<div class="flex items-center gap-2 px-3 py-2">
|
||||
<span class="text-gray-900"><%= balance.currency %> <%= balance.symbol %></span>
|
||||
<span class="text-gray-900"><%= balance.currency.iso_code %> <%= balance.currency.symbol %></span>
|
||||
<%= lucide_icon("chevron-down", class: "w-5 h-5 text-gray-500") %>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -29,7 +29,7 @@
|
|||
<%= render partial: "shared/balance_heading", locals: {
|
||||
label: "Total Value",
|
||||
period: @period,
|
||||
balance: Money.from_amount(@account.balance, @account.currency),
|
||||
balance: @account.balance_money,
|
||||
trend: @balance_series.trend
|
||||
}
|
||||
%>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<p><%= account_details[:allocation] %>%</p>
|
||||
</div>
|
||||
<div class="w-24">
|
||||
<p><%= format_currency account_details[:end_balance] %></p>
|
||||
<p><%= format_money account_details[:end_balance] %></p>
|
||||
</div>
|
||||
<div class="w-40">
|
||||
<%= render partial: "shared/trend_change", locals: { trend: account_details[:trend] } %>
|
||||
|
@ -39,7 +39,7 @@
|
|||
<p><%= account[:allocation] %>%</p>
|
||||
</div>
|
||||
<div class="w-24">
|
||||
<p><%= format_currency account[:end_balance] %></p>
|
||||
<p><%= format_money account[:end_balance] %></p>
|
||||
</div>
|
||||
<div class="w-40">
|
||||
<%= render partial: "shared/trend_change", locals: { trend: account[:trend] } %>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<%= render partial: "shared/balance_heading", locals: {
|
||||
label: "Net Worth",
|
||||
period: @period,
|
||||
balance: Money.from_amount(Current.family.net_worth, Current.family.currency),
|
||||
balance: Current.family.net_worth_money,
|
||||
trend: @net_worth_series.trend
|
||||
}
|
||||
%>
|
||||
|
@ -26,7 +26,7 @@
|
|||
<%= render partial: "shared/balance_heading", locals: {
|
||||
label: "Assets",
|
||||
period: @period,
|
||||
balance: Money.from_amount(Current.family.assets, Current.family.currency),
|
||||
balance: Current.family.assets_money,
|
||||
trend: @asset_series.trend
|
||||
} %>
|
||||
</div>
|
||||
|
@ -44,7 +44,7 @@
|
|||
label: "Liabilities",
|
||||
period: @period,
|
||||
size: "md",
|
||||
balance: Money.from_amount(Current.family.liabilities, Current.family.currency),
|
||||
balance: Current.family.liabilities_money,
|
||||
trend: @liability_series.trend
|
||||
} %>
|
||||
</div>
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
<div class="space-y-2">
|
||||
<p class="text-sm text-gray-500"><%= label %></p>
|
||||
<p class="text-gray-900 -space-x-0.5">
|
||||
<span class="text-gray-500"><%= balance.symbol %></span>
|
||||
<span class="<%= size == "lg" ? "text-xl" : "text-lg" %> font-medium"><%= format_currency(balance.amount, precision: 0, unit: '') %></span>
|
||||
<%- if balance.precision.positive? -%>
|
||||
<span class="text-gray-500"><%= balance.currency.symbol %></span>
|
||||
<span class="<%= size == "lg" ? "text-xl" : "text-lg" %> font-medium"><%= format_money_without_symbol balance, precision: 0 %></span>
|
||||
<%- if balance.currency.default_precision.positive? -%>
|
||||
<span class="text-gray-500">
|
||||
<%= balance.separator %><%= balance.cents %>
|
||||
<%= balance.currency.separator %><%= balance.cents_str %>
|
||||
</span>
|
||||
<% end %>
|
||||
</p>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<% if trend.direction == "flat" %>
|
||||
<span>No change</span>
|
||||
<% else %>
|
||||
<span><%= styles[:symbol] %><%= format_currency(trend.amount.abs) %></span>
|
||||
<span><%= styles[:symbol] %><%= format_money trend.amount.abs %></span>
|
||||
<span>(<%= lucide_icon(styles[:icon], class: "w-4 h-4 align-text-bottom inline") %><%= trend.percent %>%)</span>
|
||||
<% end %>
|
||||
</p>
|
|
@ -1,7 +1,7 @@
|
|||
<%= form_with model: @transaction do |f| %>
|
||||
<%= f.collection_select :account_id, Current.family.accounts, :id, :name, { prompt: "Select an Account", label: "Account" } %>
|
||||
<%= f.date_field :date, label: "Date" %>
|
||||
<%= f.text_field :name, label: "Name" %>
|
||||
<%= f.number_field :amount, label: "Amount", step: :any, placeholder: number_to_currency(0), in: 0.00..100000000.00 %>
|
||||
<%= f.text_field :name, label: "Name", placeholder: "Groceries" %>
|
||||
<%= f.money_field :amount_money, label: "Amount" %>
|
||||
<%= f.submit %>
|
||||
<% end %>
|
||||
|
|
|
@ -12,6 +12,6 @@
|
|||
<p><%= transaction.account.name %></p>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<p class="<%= transaction.amount < 0 ? "text-green-600" : "" %>"><%= number_to_currency(-transaction.amount, { precision: 2 }) %></p>
|
||||
<p class="<%= transaction.amount < 0 ? "text-green-600" : "" %>"><%= format_money -transaction.amount_money %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="bg-gray-25 rounded-xl p-1">
|
||||
<div class="py-2 px-4 flex items-center justify-between font-medium text-xs text-gray-500">
|
||||
<h4><%= date.strftime('%b %d, %Y') %> · <%= transactions.size %></h4>
|
||||
<span><%= number_to_currency(-transactions.sum(&:amount)) %></span>
|
||||
<span><%= format_money -transactions.sum(&:amount_money) %></span>
|
||||
</div>
|
||||
<div class="bg-white shadow-xs rounded-md border border-alpha-black-25 divide-y divide-alpha-black-50">
|
||||
<%= render partial: "transactions/transaction", collection: transactions %>
|
||||
|
|
|
@ -23,13 +23,13 @@
|
|||
<div class="p-4 space-y-2">
|
||||
<p class="text-sm text-gray-500">Income</p>
|
||||
<p class="text-gray-900 font-medium text-xl">
|
||||
<%= number_to_currency(@transactions.select { |t| t.amount < 0 }.sum(&:amount).abs, precision: 2) %>
|
||||
<%= format_money @transactions.inflows.sum(&:amount_money).abs %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4 space-y-2">
|
||||
<p class="text-sm text-gray-500">Expenses</p>
|
||||
<p class="text-gray-900 font-medium text-xl">
|
||||
<%= number_to_currency(@transactions.select { |t| t.amount >= 0 }.sum(&:amount), precision: 2) %>
|
||||
<%= format_money @transactions.outflows.sum(&:amount_money) %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -50,7 +50,6 @@
|
|||
</div>
|
||||
<div>
|
||||
<%= form.select :date, options_for_select([['All', 'all'], ['7D', 'last_7_days'], ['1M', 'last_30_days'], ["1Y", "last_365_days"]], selected: params.dig(:q, :date)), {}, { class: "block h-full w-full border border-gray-200 rounded-lg text-sm py-2 pr-8 pl-2", "data-transactions-search-target": "date" } %>
|
||||
|
||||
<!-- Hidden fields for date range -->
|
||||
<%= form.hidden_field :date_gteq, value: '', "data-transactions-search-target": "dateGteq" %>
|
||||
<%= form.hidden_field :date_lteq, value: '', "data-transactions-search-target": "dateLteq" %>
|
||||
|
@ -76,14 +75,12 @@
|
|||
<p>amount</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<% @transactions.group_by { |transaction| transaction.date }.each do |date, grouped_transactions| %>
|
||||
<%= render partial: "transactions/transaction_group", locals: { date: date, transactions: grouped_transactions } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @pagy.pages > 1 %>
|
||||
<nav class="flex items-center justify-center px-4 mt-4 sm:px-0">
|
||||
<%= render partial: "transactions/pagination", locals: { pagy: @pagy } %>
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
<%= sidebar_modal do %>
|
||||
<h3 class="font-medium mb-1">
|
||||
<span class="text-2xl"><%=format_currency @transaction.amount %></span>
|
||||
<span class="text-2xl"><%=format_money @transaction.amount_money %></span>
|
||||
<span class="text-lg text-gray-500"><%= @transaction.currency %></span>
|
||||
</h3>
|
||||
<span class="text-sm text-gray-500"><%= @transaction.date.strftime("%A %d %B") %></span>
|
||||
|
||||
<%= form_with model: @transaction, html: {data: {controller: "auto-submit-form"}} do |f| %>
|
||||
<details class="group" open>
|
||||
<summary class="list-none bg-gray-25 rounded-xl py-1 mt-4 group-open:mb-2">
|
||||
|
@ -14,12 +13,10 @@
|
|||
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<%= f.date_field :date, label: "Date" %>
|
||||
<div class="h-2"></div>
|
||||
<%= f.collection_select :account_id, Current.family.accounts, :id, :name, { prompt: "Select an Account", label: "Account", class: "text-gray-400" }, {class: "form-field__input cursor-not-allowed text-gray-400", disabled: "disabled"} %>
|
||||
</details>
|
||||
|
||||
<details class="group" open>
|
||||
<summary class="list-none bg-gray-25 rounded-xl py-1 mt-6 group-open:mb-2">
|
||||
<div class="py-2 px-[11px] flex items-center justify-between font-medium text-xs text-gray-500">
|
||||
|
@ -28,10 +25,8 @@
|
|||
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<%= f.text_field :name, label: "Name" %>
|
||||
</details>
|
||||
|
||||
<details class="group" open>
|
||||
<summary class="list-none bg-gray-25 rounded-xl py-1 mt-6 group-open:mb-2">
|
||||
<div class="py-2 px-[11px] flex items-center justify-between font-medium text-xs text-gray-500">
|
||||
|
@ -40,7 +35,6 @@
|
|||
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<label class="flex items-center cursor-pointer justify-between mx-3">
|
||||
<%= f.check_box :excluded, class: "sr-only peer" %>
|
||||
<div class="flex flex-col justify-center text-sm w-[340px] py-3">
|
||||
|
@ -50,7 +44,6 @@
|
|||
<div class="relative w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-100 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
</details>
|
||||
|
||||
<details class="group" open>
|
||||
<summary class="list-none bg-gray-25 rounded-xl py-1 mt-6 mb-2">
|
||||
<div class="py-2 px-[11px] flex items-center justify-between font-medium text-xs text-gray-500">
|
||||
|
@ -59,10 +52,7 @@
|
|||
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<%= f.text_area :notes, label: "Notes", placeholder: "Enter a note" %>
|
||||
</details>
|
||||
|
||||
<% end %>
|
||||
|
||||
<% end %>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
</div>
|
||||
<div class="flex items-center justify-between grow">
|
||||
<%= f.date_field :date, required: 'required', 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 :value, step: :any, placeholder: number_to_currency(0), in: 0.00..100000000.00, required: 'required', 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.number_field :value, required: 'required', placeholder: "0.00", class: "bg-white border border-alpha-black-200 rounded-lg shadow-xs text-gray-900 text-sm px-3 py-1.5 text-right" %>
|
||||
</div>
|
||||
<div class="w-[296px] flex gap-2 justify-end items-center">
|
||||
<%= link_to "Cancel", account_path(@valuation.account), class: "text-sm text-gray-900 hover:text-gray-800 font-medium px-3 py-1.5" %>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue