1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-20 13:49:39 +02:00

Finalize transaction drawer, simplify money form helpers (#1191)

* Finalize transaction drawer, simplify money form helpers

* Fix money form errors

* Reusable disclosure helper, fix styles

* Final style tweaks
This commit is contained in:
Zach Gollwitzer 2024-09-20 08:38:19 -04:00 committed by GitHub
parent 730e58d763
commit 5942ce7e3c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 275 additions and 214 deletions

View file

@ -20,14 +20,14 @@
} }
.form-field__label { .form-field__label {
@apply block text-xs text-gray-500; @apply block text-xs text-gray-500 peer-disabled:text-gray-400;
} }
.form-field__input { .form-field__input {
@apply border-none bg-transparent text-sm opacity-100 w-full p-0; @apply border-none bg-transparent text-sm opacity-100 w-full p-0;
@apply focus:opacity-100 focus:outline-none focus:ring-0; @apply focus:opacity-100 focus:outline-none focus:ring-0;
@apply placeholder-shown:opacity-50; @apply placeholder-shown:opacity-50;
@apply disabled:opacity-50; @apply disabled:text-gray-400;
} }
.form-field__radio { .form-field__radio {
@ -63,12 +63,16 @@
} }
select[multiple="multiple"] option { select[multiple="multiple"] option {
@apply p-2 rounded-md; @apply py-2 rounded-md;
} }
select[multiple="multiple"] option:checked { select[multiple="multiple"] option:checked {
@apply bg-gray-50; @apply after:content-['\2713'] bg-white after:text-gray-500 after:ml-2;
@apply after:content-['\2713'] after:float-right after:text-gray-500; }
select[multiple="multiple"] option:active,
select[multiple="multiple"] option:focus {
@apply bg-white;
} }
.maybe-switch { .maybe-switch {

View file

@ -17,6 +17,9 @@ class TransactionsController < ApplicationController
@entry = Current.family.entries.new(entryable: Account::Transaction.new).tap do |e| @entry = Current.family.entries.new(entryable: Account::Transaction.new).tap do |e|
if params[:account_id] if params[:account_id]
e.account = Current.family.accounts.find(params[:account_id]) e.account = Current.family.accounts.find(params[:account_id])
e.currency = e.account.currency
else
e.currency = Current.family.currency
end end
end end
end end

View file

@ -57,6 +57,11 @@ module ApplicationHelper
render partial: "shared/drawer", locals: { content: content } render partial: "shared/drawer", locals: { content: content }
end end
def disclosure(title, &block)
content = capture &block
render partial: "shared/disclosure", locals: { title: title, content: content }
end
def sidebar_link_to(name, path, options = {}) def sidebar_link_to(name, path, options = {})
is_current = current_page?(path) || (request.path.start_with?(path) && path != "/") is_current = current_page?(path) || (request.path.start_with?(path) && path != "/")

View file

@ -10,11 +10,6 @@ module FormsHelper
render partial: "shared/modal_form", locals: { title:, subtitle:, content: } render partial: "shared/modal_form", locals: { title:, subtitle:, content: }
end end
def form_field_tag(options = {}, &block)
options[:class] = [ "form-field", options[:class] ].compact.join(" ")
tag.div(**options, &block)
end
def radio_tab_tag(form:, name:, value:, label:, icon:, checked: false, disabled: false) def radio_tab_tag(form:, name:, value:, label:, icon:, checked: false, disabled: false)
form.label name, for: form.field_id(name, value), class: "group has-[:disabled]:cursor-not-allowed" do form.label name, for: form.field_id(name, value), class: "group has-[:disabled]:cursor-not-allowed" do
concat radio_tab_contents(label:, icon:) concat radio_tab_contents(label:, icon:)
@ -27,56 +22,11 @@ module FormsHelper
form.select(:period, periods_for_select, { selected: selected }, class: classes, data: { "auto-submit-form-target": "auto" }) form.select(:period, periods_for_select, { selected: selected }, class: classes, data: { "auto-submit-form-target": "auto" })
end end
def money_with_currency_field(form, money_method, options = {}) def currencies_for_select
render partial: "shared/money_field", locals: { Money::Currency.all_instances.sort_by(&:priority)
form: form,
money_method: money_method,
default_currency: options[:default_currency] || "USD",
disable_currency: options[:disable_currency] || false,
hide_currency: options[:hide_currency] || false,
label: options[:label] || "Amount",
required: options[:required] || false
}
end
def money_field(form, method, options = {})
value = form.object ? form.object.send(method) : nil
currency = value&.currency || Money::Currency.new(options[:default_currency] || "USD")
# See "Monetizable" concern
money_amount_method = method.to_s.chomp("_money").to_sym
money_options = {
value: value&.amount,
placeholder: "100",
min: -99999999999999,
max: 99999999999999,
step: currency.step
}
merged_options = options.merge(money_options)
form.number_field money_amount_method, merged_options
end
def currency_select_full(form, method, options = {}, html_options = {}, &block)
choices = currencies_for_select.map { |currency| [ "#{currency.name} (#{currency.iso_code})", currency.iso_code ] }
form.select method, choices, options, html_options, &block
end
def currency_select(form, method, options = {}, html_options = {}, &block)
choices = currencies_for_select.map(&:iso_code)
form.select method, choices, options, html_options, &block
end end
private private
def currencies_for_select
Money::Currency.all_instances
.sort_by(&:priority)
end
def radio_tab_contents(label:, icon:) def radio_tab_contents(label:, icon:)
tag.div(class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-gray-400 group-has-[:checked]:bg-white group-has-[:checked]:text-gray-800 group-has-[:checked]:shadow-sm") do tag.div(class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-gray-400 group-has-[:checked]:bg-white group-has-[:checked]:text-gray-800 group-has-[:checked]:shadow-sm") do
concat lucide_icon(icon, class: "w-5 h-5") concat lucide_icon(icon, class: "w-5 h-5")

View file

@ -6,53 +6,69 @@ class StyledFormBuilder < ActionView::Helpers::FormBuilder
text_field_helpers.each do |selector| text_field_helpers.each do |selector|
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{selector}(method, options = {}) def #{selector}(method, options = {})
input_html = label_html(method, options) + super(method, merged_options(options)) merged_options = { class: "form-field__input" }.merge(options)
input_html = apply_form_field_wrapper(input_html) unless options[:inline] label = build_label(method, options)
input_html field = super(method, merged_options)
build_styled_field(label, field, merged_options)
end end
RUBY_EVAL RUBY_EVAL
end end
def radio_button(method, tag_value, options = {}) def radio_button(method, tag_value, options = {})
super(method, tag_value, merged_options(options, "form-field__radio")) merged_options = { class: "form-field__radio" }.merge(options)
super(method, tag_value, merged_options)
end end
def select(method, choices, options = {}, html_options = {}) def select(method, choices, options = {}, html_options = {})
input_html = label_html(method, options) + super(method, choices, options, merged_options(html_options)) merged_html_options = { class: "form-field__input" }.merge(html_options)
input_html = apply_form_field_wrapper(input_html, class: "pr-0") unless options[:inline]
input_html label = build_label(method, options)
field = super(method, choices, options, merged_html_options)
build_styled_field(label, field, options, remove_padding_right: true)
end end
def collection_select(method, collection, value_method, text_method, options = {}, html_options = {}) def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
input_html = label_html(method, options) + super(method, collection, value_method, text_method, options, merged_options(html_options)) merged_html_options = { class: "form-field__input" }.merge(html_options)
input_html = apply_form_field_wrapper(input_html, class: "pr-0") unless options[:inline]
input_html label = build_label(method, options)
field = super(method, collection, value_method, text_method, options, merged_html_options)
build_styled_field(label, field, options, remove_padding_right: true)
end
def money_field(amount_method, currency_method, options = {})
@template.render partial: "shared/money_field", locals: {
form: self,
amount_method:,
currency_method:,
**options
}
end end
def submit(value = nil, options = {}) def submit(value = nil, options = {})
merged_options = { class: "form-field__submit" }.merge(options)
value, options = nil, value if value.is_a?(Hash) value, options = nil, value if value.is_a?(Hash)
super(value, merged_options(options, "form-field__submit")) super(value, merged_options)
end end
private private
def apply_form_field_wrapper(input_html, **options) def build_styled_field(label, field, options, remove_padding_right: false)
@template.form_field_tag(**options) do if options[:inline]
input_html label + field
else
@template.tag.div class: [ "form-field", options[:container_class], ("pr-0" if remove_padding_right) ] do
label + field
end
end end
end end
def merged_options(options, default_class = "form-field__input") def build_label(method, options)
combined_classes = options.fetch(:class, "") + " #{default_class}"
style_options = { class: combined_classes }
non_custom_options = options.except(:class, :label, :inline)
style_options.merge(non_custom_options)
end
def label_html(method, options)
return label(method, class: "form-field__label") if options[:label] == true
return "".html_safe unless options[:label] return "".html_safe unless options[:label]
return label(method, class: "form-field__label") if options[:label] == true
label(method, options[:label], class: "form-field__label") label(method, options[:label], class: "form-field__label")
end end
end end

View file

@ -13,7 +13,6 @@ export default class extends Controller {
updateAmount(currency) { updateAmount(currency) {
(new CurrenciesService).get(currency).then((currency) => { (new CurrenciesService).get(currency).then((currency) => {
console.log(currency)
this.amountTarget.step = currency.step; this.amountTarget.step = currency.step;
if (isFinite(this.amountTarget.value)) { if (isFinite(this.amountTarget.value)) {

View file

@ -2,7 +2,7 @@
<%= styled_form_with data: { turbo_frame: "_top", controller: "trade-form" }, <%= styled_form_with data: { turbo_frame: "_top", controller: "trade-form" },
scope: :account_entry, scope: :account_entry,
url: entry.new_record? ? account_trades_path(entry.account) : account_entry_path(entry.account, entry) do |form| %> url: account_trades_path(entry.account) do |form| %>
<div class="space-y-4"> <div class="space-y-4">
<div class="space-y-2"> <div class="space-y-2">
<%= form.select :type, options_for_select([%w[Buy buy], %w[Sell sell], %w[Deposit transfer_in], %w[Withdrawal transfer_out], %w[Interest interest]], "buy"), { label: t(".type") }, { data: { "trade-form-target": "typeInput" } } %> <%= form.select :type, options_for_select([%w[Buy buy], %w[Sell sell], %w[Deposit transfer_in], %w[Withdrawal transfer_out], %w[Interest interest]], "buy"), { label: t(".type") }, { data: { "trade-form-target": "typeInput" } } %>
@ -13,7 +13,7 @@
<%= form.date_field :date, label: true %> <%= form.date_field :date, label: true %>
<div data-trade-form-target="amountInput" hidden> <div data-trade-form-target="amountInput" hidden>
<%= money_with_currency_field form, :amount_money, label: t(".amount"), disable_currency: true %> <%= form.money_field :amount, :currency, label: t(".amount"), disable_currency: true %>
</div> </div>
<div data-trade-form-target="transferAccountInput" hidden> <div data-trade-form-target="transferAccountInput" hidden>
@ -25,7 +25,7 @@
</div> </div>
<div data-trade-form-target="priceInput"> <div data-trade-form-target="priceInput">
<%= money_with_currency_field form, :price_money, label: t(".price"), disable_currency: true %> <%= form.money_field :price, :currency, label: t(".price"), disable_currency: true %>
</div> </div>
</div> </div>

View file

@ -1,119 +1,162 @@
<% entry, transaction, account = @entry, @entry.account_transaction, @entry.account %> <% entry, transaction, account = @entry, @entry.account_transaction, @entry.account %>
<%= drawer do %> <%= drawer do %>
<div> <header class="mb-4 space-y-1">
<header class="mb-4 space-y-1"> <div class="flex items-center gap-4">
<div class="flex items-center gap-4"> <h3 class="font-medium">
<h3 class="font-medium"> <span class="text-2xl">
<span class="text-2xl"><%= format_money -entry.amount_money %></span> <%= format_money -entry.amount_money %>
<span class="text-lg text-gray-500"><%= entry.currency %></span> </span>
</h3>
<% if entry.marked_as_transfer? %> <span class="text-lg text-gray-500">
<%= lucide_icon "arrow-left-right", class: "text-gray-500 mt-1 w-5 h-5" %> <%= entry.currency %>
<% end %> </span>
</div> </h3>
<span class="text-sm text-gray-500"><%= entry.date.strftime("%A %d %B") %></span> <% if entry.marked_as_transfer? %>
</header> <%= lucide_icon "arrow-left-right", class: "text-gray-500 mt-1 w-5 h-5" %>
<% end %>
</div>
<div class="space-y-2"> <span class="text-sm text-gray-500">
<details class="group space-y-2" open> <%= entry.date.strftime("%A %d %B") %>
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none"> </span>
<h4><%= t(".overview") %></h4> </header>
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
</summary>
<div class="pb-6"> <div class="space-y-2">
<%= styled_form_with model: [account, entry], url: account_transaction_path(account, entry), class: "space-y-2", data: { controller: "auto-submit-form" } do |f| %> <!-- Overview Section -->
<%= f.text_field :name, label: t(".name_label"), "data-auto-submit-form-target": "auto" %> <%= disclosure t(".overview") do %>
<% unless entry.marked_as_transfer? %> <div class="pb-4">
<div class="flex space-x-2"> <%= styled_form_with model: [account, entry],
<div> url: account_transaction_path(account, entry),
<%= f.select :nature, [["Expense", "expense"], ["Income", "income"]], { label: t(".nature"), selected: entry.amount.negative? ? "income" : "expense" }, "data-auto-submit-form-target": "auto" %> class: "space-y-2",
</div> data: { controller: "auto-submit-form" } do |f| %>
<div class="flex-grow"> <%= f.text_field :name,
<%= f.number_field :amount, value: entry.amount.abs, label: t(".amount"), step: "0.01", "data-auto-submit-form-target": "auto", "data-autosubmit-trigger-event": "change" %> label: t(".name_label"),
</div> "data-auto-submit-form-target": "auto" %>
</div>
<% end %>
<%= f.date_field :date, label: t(".date_label"), max: Date.current, "data-auto-submit-form-target": "auto" %>
<%= f.fields_for :entryable do |ef| %> <%= f.date_field :date,
<% unless entry.marked_as_transfer? %> label: t(".date_label"),
<%= ef.collection_select :category_id, Current.family.categories.alphabetically, :id, :name, { prompt: t(".category_placeholder"), label: t(".category_label"), class: "text-gray-400" }, "data-auto-submit-form-target": "auto" %> max: Date.current,
<%= ef.collection_select :merchant_id, Current.family.merchants.alphabetically, :id, :name, { prompt: t(".merchant_placeholder"), label: t(".merchant_label"), class: "text-gray-400" }, "data-auto-submit-form-target": "auto" %> "data-auto-submit-form-target": "auto" %>
<% end %>
<% end %>
<%= f.collection_select :account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".account_placeholder"), label: t(".account_label"), class: "text-gray-500" }, { class: "form-field__input cursor-not-allowed text-gray-400", disabled: "disabled" } %>
<% end %>
</div>
</details>
<details class="group space-y-2" open>
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
<h4><%= t(".additional") %></h4>
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
</summary>
<div class="pb-6">
<%= styled_form_with model: [account, entry], url: account_transaction_path(account, entry), class: "space-y-2", data: { controller: "auto-submit-form" } do |f| %>
<%= f.fields_for :entryable do |ef| %>
<%= ef.select :tag_ids,
options_for_select(Current.family.tags.alphabetically.pluck(:name, :id), transaction.tag_ids),
{
multiple: true,
label: t(".tags_label"),
class: "placeholder:text-gray-500"
},
"data-auto-submit-form-target": "auto" %>
<%= ef.text_area :notes, label: t(".note_label"), placeholder: t(".note_placeholder"), "data-auto-submit-form-target": "auto" %>
<% end %>
<% end %>
</div>
</details>
<details class="group space-y-2" open>
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
<h4><%= t(".settings") %></h4>
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
</summary>
<div class="pb-6">
<%= styled_form_with model: [account, entry], url: account_transaction_path(account, entry), class: "p-3 space-y-3", data: { controller: "auto-submit-form" } do |f| %>
<%= f.fields_for :entryable do |ef| %>
<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">
<%= ef.check_box :excluded, class: "sr-only peer", "data-auto-submit-form-target": "auto" %>
<label for="account_entry_entryable_attributes_excluded" class="maybe-switch"></label>
</div>
</div>
<% end %>
<% end %>
<% unless entry.marked_as_transfer? %> <% unless entry.marked_as_transfer? %>
<div class="flex items-center justify-between gap-2 p-3"> <div class="flex items-center gap-2">
<div class="text-sm space-y-1"> <%= f.select :nature,
<h4 class="text-gray-900"><%= t(".delete_title") %></h4> [["Expense", "expense"], ["Income", "income"]],
<p class="text-gray-500"><%= t(".delete_subtitle") %></p> { container_class: "w-1/3", label: t(".nature"), selected: entry.amount.negative? ? "income" : "expense" },
</div> { data: { "auto-submit-form-target": "auto" } } %>
<%= button_to t(".delete"), <%= f.money_field :amount, :currency, label: t(".amount"),
account_entry_path(account, entry), container_class: "w-2/3",
method: :delete, auto_submit: true,
class: "rounded-lg px-3 py-2 text-red-500 text-sm font-medium border border-alpha-black-200", min: 0,
data: { turbo_confirm: true, turbo_frame: "_top" } %> value: entry.amount.abs %>
</div> </div>
<% end %> <% end %>
</div>
</details> <%= f.select :account,
</div> options_for_select(
Current.family.accounts.alphabetically.pluck(:name, :id),
entry.account_id
),
{ label: t(".account_label") },
{ disabled: true } %>
<% end %>
</div>
<% end %>
<!-- Details Section -->
<%= disclosure t(".details") do %>
<div class="pb-4">
<%= styled_form_with model: [account, entry],
url: account_transaction_path(account, entry),
class: "space-y-2",
data: { controller: "auto-submit-form" } do |f| %>
<%= f.fields_for :entryable do |ef| %>
<% unless entry.marked_as_transfer? %>
<%= ef.collection_select :category_id,
Current.family.categories.alphabetically,
:id, :name,
{ prompt: t(".category_placeholder"),
label: t(".category_label"),
class: "text-gray-400" },
"data-auto-submit-form-target": "auto" %>
<%= ef.collection_select :merchant_id,
Current.family.merchants.alphabetically,
:id, :name,
{ prompt: t(".merchant_placeholder"),
label: t(".merchant_label"),
class: "text-gray-400" },
"data-auto-submit-form-target": "auto" %>
<% end %>
<%= ef.select :tag_ids,
options_for_select(
Current.family.tags.alphabetically.pluck(:name, :id),
transaction.tag_ids
),
{
multiple: true,
label: t(".tags_label"),
container_class: "h-40"
},
{ "data-auto-submit-form-target": "auto" } %>
<%= ef.text_area :notes,
label: t(".note_label"),
placeholder: t(".note_placeholder"),
rows: 5,
"data-auto-submit-form-target": "auto" %>
<% end %>
<% end %>
</div>
<% end %>
<!-- Settings Section -->
<%= disclosure t(".settings") do %>
<div class="pb-4">
<!-- Exclude Transaction Form -->
<%= styled_form_with model: [account, entry],
url: account_transaction_path(account, entry),
class: "p-3",
data: { controller: "auto-submit-form" } do |f| %>
<%= f.fields_for :entryable do |ef| %>
<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">
<%= ef.check_box :excluded,
class: "sr-only peer",
"data-auto-submit-form-target": "auto" %>
<label for="account_entry_entryable_attributes_excluded"
class="maybe-switch"></label>
</div>
</div>
<% end %>
<% 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>
<%= 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>
<% end %>
</div> </div>
<% end %> <% end %>

View file

@ -29,7 +29,7 @@
<%= f.text_field :name, value: transfer.name, label: t(".description"), placeholder: t(".description_placeholder"), required: true %> <%= f.text_field :name, value: transfer.name, label: t(".description"), placeholder: t(".description_placeholder"), required: true %>
<%= f.collection_select :from_account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".select_account"), label: t(".from") }, required: true %> <%= f.collection_select :from_account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".select_account"), label: t(".from") }, required: true %>
<%= f.collection_select :to_account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".select_account"), label: t(".to") }, required: true %> <%= f.collection_select :to_account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".select_account"), label: t(".to") }, required: true %>
<%= money_field f, :amount_money, label: t(".amount"), required: true %> <%= f.money_field :amount, :currency, label: t(".amount"), required: true %>
<%= f.date_field :date, value: transfer.date, label: t(".date"), required: true, max: Date.current %> <%= f.date_field :date, value: transfer.date, label: t(".date"), required: true, max: Date.current %>
</section> </section>

View file

@ -5,7 +5,7 @@
<%= f.hidden_field :accountable_type %> <%= f.hidden_field :accountable_type %>
<%= f.text_field :name, placeholder: t(".name_placeholder"), required: "required", label: t(".name_label"), autofocus: true %> <%= f.text_field :name, placeholder: t(".name_placeholder"), required: "required", label: t(".name_label"), autofocus: true %>
<%= f.collection_select :institution_id, Current.family.institutions.alphabetically, :id, :name, { include_blank: t(".ungrouped"), label: t(".institution") } %> <%= f.collection_select :institution_id, Current.family.institutions.alphabetically, :id, :name, { include_blank: t(".ungrouped"), label: t(".institution") } %>
<%= money_with_currency_field f, :balance_money, label: t(".balance"), required: true, default_currency: Current.family.currency %> <%= f.money_field :balance, :currency, label: t(".balance"), required: true, default_currency: Current.family.currency %>
<% if account.new_record? %> <% if account.new_record? %>
<div class="flex items-center gap-2 mt-3 mb-6"> <div class="flex items-center gap-2 mt-3 mb-6">

View file

@ -8,7 +8,10 @@
<div> <div>
<%= styled_form_with model: Current.user, url: settings_preferences_path, class: "space-y-4", data: { controller: "auto-submit-form" } do |form| %> <%= styled_form_with model: Current.user, url: settings_preferences_path, class: "space-y-4", data: { controller: "auto-submit-form" } do |form| %>
<%= form.fields_for :family_attributes do |family_form| %> <%= form.fields_for :family_attributes do |family_form| %>
<%= currency_select_full family_form, :currency, { label: "Currency", selected: Current.family.currency }, { data: { auto_submit_form_target: "auto" } } %> <%= family_form.select :currency,
currencies_for_select.map { |currency| [ "#{currency.name} (#{currency.iso_code})", currency.iso_code ] },
{ label: "Currency", selected: Current.family.currency },
{ data: { auto_submit_form_target: "auto" } } %>
<% end %> <% end %>
<% end %> <% end %>
</div> </div>

View file

@ -0,0 +1,12 @@
<%# locals: (title:, content:, open: true) %>
<details class="group space-y-2" <%= "open" if open %>>
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium
uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
<h3><%= title %></h3>
<%= lucide_icon "chevron-down",
class: "group-open:transform group-open:rotate-180 text-gray-500 w-5 h-5" %>
</summary>
<%= content %>
</details>

View file

@ -1,26 +1,53 @@
<%# locals: (form:, money_method:, default_currency:, disable_currency: false, hide_currency: false, label: nil, required: false) %> <%# locals: (form:, amount_method:, currency_method:, **options) %>
<% fallback_label = t(".money-label") %> <% currency_value = if form.object && form.object.respond_to?(currency_method)
<% currency = form.object ? (form.object.send(money_method)&.currency || Money::Currency.new(default_currency)) : Money::Currency.new(default_currency) %> form.object.public_send(currency_method)
end
currency = Money::Currency.new(currency_value || options[:default_currency] || "USD") %>
<div class="form-field pr-0" data-controller="money-field"> <div class="form-field pr-0 <%= options[:container_class] %>" data-controller="money-field">
<%= form.label label || fallback_label, { class: "form-field__label" } %> <%= form.label options[:label] || t(".label"), class: "form-field__label" %>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<div class="flex items-center grow gap-1"> <div class="flex items-center grow gap-1">
<span class="text-gray-500 text-sm" data-money-field-target="symbol"><%= currency.symbol %></span> <span class="text-gray-500 text-sm" data-money-field-target="symbol">
<%= money_field form, money_method, { inline: true, "data-money-field-target" => "amount", default_currency: currency, required: required } %> <%= currency.symbol %>
</span>
<%= form.number_field amount_method,
class: "form-field__input",
inline: true,
placeholder: "100",
value: if options[:value]
sprintf("%.#{currency.default_precision}f", options[:value])
elsif form.object && form.object.respond_to?(amount_method)
form.object.public_send(amount_method)
end,
min: options[:min] || -99999999999999,
max: options[:max] || 99999999999999,
step: currency.step,
data: {
"money-field-target": "amount",
action: "change->money-field#handleAmountChange",
"auto-submit-form-target": ("auto" if options[:auto_submit])
}.compact,
required: options[:required] %>
</div> </div>
<% unless hide_currency %>
<% unless options[:hide_currency] %>
<div> <div>
<%= currency_select form, :currency, { inline: true, selected: currency.iso_code }, { <%= form.select currency_method,
class: "form-field__input text-right pr-8 disabled:text-gray-500", currencies_for_select.map(&:iso_code),
disabled: disable_currency, { inline: true },
data: { {
"money-field-target" => "currency", class: "w-fit pr-5 disabled:text-gray-400 form-field__input",
action: "money-field#handleCurrencyChange" disabled: options[:disable_currency],
} data: {
} %> "money-field-target": "currency",
action: "change->money-field#handleCurrencyChange",
"auto-submit-form-target": ("auto" if options[:auto_submit])
}.compact
} %>
</div> </div>
<% end %> <% end %>
</div> </div>

View file

@ -13,7 +13,7 @@
<section class="space-y-2"> <section class="space-y-2">
<%= f.text_field :name, label: t(".description"), placeholder: t(".description_placeholder"), required: true %> <%= f.text_field :name, label: t(".description"), placeholder: t(".description_placeholder"), required: true %>
<%= f.collection_select :account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".account_prompt"), label: t(".account") }, required: true %> <%= f.collection_select :account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".account_prompt"), label: t(".account") }, required: true %>
<%= money_with_currency_field f, :amount_money, label: t(".amount"), required: true, default_currency: @entry.account&.currency || Current.family.currency %> <%= f.money_field :amount, :currency, label: t(".amount"), required: true %>
<%= f.hidden_field :entryable_type, value: "Account::Transaction" %> <%= f.hidden_field :entryable_type, value: "Account::Transaction" %>
<%= f.fields_for :entryable do |ef| %> <%= f.fields_for :entryable do |ef| %>
<%= ef.collection_select :category_id, Current.family.categories.alphabetically, :id, :name, { prompt: t(".category_prompt"), label: t(".category") } %> <%= ef.collection_select :category_id, Current.family.categories.alphabetically, :id, :name, { prompt: t(".category_prompt"), label: t(".category") } %>

View file

@ -14,8 +14,6 @@ en:
longer be included in income or spending calculations. longer be included in income or spending calculations.
show: show:
account_label: Account account_label: Account
account_placeholder: Select an account
additional: Additional
amount: Amount amount: Amount
category_label: Category category_label: Category
category_placeholder: Select a category category_placeholder: Select a category
@ -24,18 +22,19 @@ en:
delete_subtitle: This permanently deletes the transaction, affects your historical delete_subtitle: This permanently deletes the transaction, affects your historical
balances, and cannot be undone. balances, and cannot be undone.
delete_title: Delete transaction delete_title: Delete transaction
details: Details
exclude_subtitle: This excludes the transaction from any in-app features or exclude_subtitle: This excludes the transaction from any in-app features or
analytics. analytics.
exclude_title: Exclude transaction exclude_title: Exclude transaction
merchant_label: Merchant merchant_label: Merchant
merchant_placeholder: Select a merchant merchant_placeholder: Select a merchant
name_label: Name name_label: Name
nature: Transaction type nature: Type
note_label: Notes note_label: Notes
note_placeholder: Enter a note note_placeholder: Enter a note
overview: Overview overview: Overview
settings: Settings settings: Settings
tags_label: Select one or more tags tags_label: Tags
transaction: transaction:
remove_transfer: Remove transfer remove_transfer: Remove transfer
remove_transfer_body: This will remove the transfer from this transaction remove_transfer_body: This will remove the transfer from this transaction

View file

@ -7,7 +7,7 @@ en:
cancel: Cancel cancel: Cancel
title: Are you sure? title: Are you sure?
money_field: money_field:
money-label: Amount label: Amount
no_account_empty_state: no_account_empty_state:
new_account: New account new_account: New account
no_account_subtitle: Since no accounts have been added, there's no data to display. no_account_subtitle: Since no accounts have been added, there's no data to display.