mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-09 23:45:21 +02:00
Improve account transaction, trade, and valuation editing and sync experience (#1506)
* Consolidate entry controller logic * Transaction builder * Update trades controller to use new params * Load account charts in turbo frames, fix PG overflow * Consolidate tests * Tests passing * Remove unused code * Add client side trade form validations
This commit is contained in:
parent
76f2714006
commit
c3248cd796
97 changed files with 1103 additions and 1159 deletions
|
@ -1,35 +1,51 @@
|
|||
<%# locals: (entry:) %>
|
||||
|
||||
<%= styled_form_with data: { turbo_frame: "_top", controller: "trade-form" },
|
||||
model: entry,
|
||||
scope: :account_entry,
|
||||
url: account_trades_path(entry.account) do |form| %>
|
||||
<% type = params[:type] || "buy" %>
|
||||
|
||||
<%= styled_form_with model: entry, url: account_trades_path, data: { controller: "trade-form" } do |form| %>
|
||||
|
||||
<%= form.hidden_field :account_id %>
|
||||
|
||||
<div class="space-y-4">
|
||||
<% if entry.errors.any? %>
|
||||
<%= render "shared/form_errors", model: entry %>
|
||||
<% end %>
|
||||
|
||||
<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" } } %>
|
||||
<div data-trade-form-target="tickerInput">
|
||||
<%= form.select :type, [
|
||||
["Buy", "buy"],
|
||||
["Sell", "sell"],
|
||||
["Deposit", "deposit"],
|
||||
["Withdrawal", "withdrawal"],
|
||||
["Interest", "interest"]
|
||||
],
|
||||
{ label: t(".type"), selected: type },
|
||||
{ data: {
|
||||
action: "trade-form#changeType",
|
||||
trade_form_url_param: new_account_trade_path(account_id: entry.account_id),
|
||||
trade_form_key_param: "type",
|
||||
}} %>
|
||||
|
||||
<% if %w[buy sell].include?(type) %>
|
||||
<div class="form-field combobox">
|
||||
<%= form.combobox :ticker, securities_account_trades_path(entry.account), label: t(".holding"), placeholder: t(".ticker_placeholder") %>
|
||||
<%= form.combobox :ticker, securities_path(country_code: Current.family.country), label: t(".holding"), placeholder: t(".ticker_placeholder"), required: true %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= form.date_field :date, label: true, value: Date.today %>
|
||||
<%= form.date_field :date, label: true, value: Date.today, required: true %>
|
||||
|
||||
<div data-trade-form-target="amountInput" hidden>
|
||||
<%= form.money_field :amount, label: t(".amount") %>
|
||||
</div>
|
||||
<% unless %w[buy sell].include?(type) %>
|
||||
<%= form.money_field :amount, label: t(".amount"), required: true %>
|
||||
<% end %>
|
||||
|
||||
<div data-trade-form-target="transferAccountInput" hidden>
|
||||
<% if %w[deposit withdrawal].include?(type) %>
|
||||
<%= form.collection_select :transfer_account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".account_prompt"), label: t(".account") } %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div data-trade-form-target="qtyInput">
|
||||
<%= form.number_field :qty, label: t(".qty"), placeholder: "10", min: 0.000000000000000001, step: "any" %>
|
||||
</div>
|
||||
|
||||
<div data-trade-form-target="priceInput">
|
||||
<%= form.money_field :price, label: t(".price"), currency_value_override: "USD", disable_currency: true %>
|
||||
</div>
|
||||
<% if %w[buy sell].include?(type) %>
|
||||
<%= form.number_field :qty, label: t(".qty"), placeholder: "10", min: 0.000000000000000001, step: "any", required: true %>
|
||||
<%= form.money_field :price, label: t(".price"), required: true %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= form.submit t(".submit") %>
|
||||
|
|
68
app/views/account/trades/_header.html.erb
Normal file
68
app/views/account/trades/_header.html.erb
Normal file
|
@ -0,0 +1,68 @@
|
|||
<%# locals: (entry:) %>
|
||||
|
||||
<div id="<%= dom_id(entry, :header) %>">
|
||||
<%= tag.header class: "mb-4 space-y-1" do %>
|
||||
<span class="text-gray-500 text-sm">
|
||||
<%= entry.amount.negative? ? t(".sell") : t(".buy") %>
|
||||
</span>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<h3 class="font-medium">
|
||||
<span class="text-2xl">
|
||||
<%= format_money entry.amount_money %>
|
||||
</span>
|
||||
|
||||
<span class="text-lg text-gray-500">
|
||||
<%= entry.currency %>
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<span class="text-sm text-gray-500">
|
||||
<%= I18n.l(entry.date, format: :long) %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<% trade = entry.account_trade %>
|
||||
|
||||
<div class="mb-2">
|
||||
<%= disclosure t(".overview") do %>
|
||||
<div class="pb-4">
|
||||
<dl class="space-y-3 px-3 py-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-gray-500"><%= t(".symbol_label") %></dt>
|
||||
<dd class="text-gray-900"><%= trade.security.ticker %></dd>
|
||||
</div>
|
||||
|
||||
<% if trade.buy? %>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-gray-500"><%= t(".purchase_qty_label") %></dt>
|
||||
<dd class="text-gray-900"><%= trade.qty.abs %></dd>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-gray-500"><%= t(".purchase_price_label") %></dt>
|
||||
<dd class="text-gray-900"><%= format_money trade.price_money %></dd>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if trade.security.current_price.present? %>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-gray-500"><%= t(".current_market_price_label") %></dt>
|
||||
<dd class="text-gray-900"><%= format_money trade.security.current_price %></dd>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if trade.buy? && trade.unrealized_gain_loss.present? %>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-gray-500"><%= t(".total_return_label") %></dt>
|
||||
<dd style="color: <%= trade.unrealized_gain_loss.color %>;">
|
||||
<%= render "shared/trend_change", trend: trade.unrealized_gain_loss %>
|
||||
</dd>
|
||||
</div>
|
||||
<% end %>
|
||||
</dl>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
|
@ -1,11 +0,0 @@
|
|||
<div class="flex items-center">
|
||||
<%= image_tag(security.logo_url, class: "rounded-full h-8 w-8 inline-block mr-2" ) %>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm font-medium">
|
||||
<%= security.name.presence || security.symbol %>
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">
|
||||
<%= "#{security.symbol} (#{security.exchange_acronym})" %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
|
@ -6,7 +6,7 @@
|
|||
</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 %>
|
||||
<%= form_with url: bulk_delete_account_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>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<%# locals: (entry:, selectable: true, show_balance: false, origin: nil) %>
|
||||
<%# locals: (entry:, selectable: true, show_balance: false) %>
|
||||
|
||||
<% trade, account = entry.account_trade, entry.account %>
|
||||
|
||||
<div class="grid grid-cols-12 items-center text-gray-900 text-sm font-medium p-4">
|
||||
<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="col-span-8 flex items-center gap-4">
|
||||
<% if selectable %>
|
||||
<%= check_box_tag dom_id(entry, "selection"),
|
||||
|
@ -16,12 +16,12 @@
|
|||
<%= trade.name.first.upcase %>
|
||||
</div>
|
||||
|
||||
<div class="truncate text-gray-900">
|
||||
<div class="truncate">
|
||||
<% if entry.new_record? %>
|
||||
<%= content_tag :p, trade.name %>
|
||||
<% else %>
|
||||
<%= link_to trade.name,
|
||||
account_entry_path(account, entry),
|
||||
account_entry_path(entry),
|
||||
data: { turbo_frame: "drawer", turbo_prefetch: false },
|
||||
class: "hover:underline hover:text-gray-800" %>
|
||||
<% end %>
|
||||
|
@ -31,7 +31,9 @@
|
|||
</div>
|
||||
|
||||
<div class="col-span-2 justify-self-end font-medium text-sm">
|
||||
<%= tag.span format_money(entry.amount_money) %>
|
||||
<%= content_tag :p,
|
||||
format_money(-entry.amount_money),
|
||||
class: ["text-green-600": entry.amount.negative?] %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 justify-self-end">
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
<%= async_combobox_options @securities,
|
||||
render_in: { partial: "account/trades/security" } %>
|
|
@ -1,83 +1,37 @@
|
|||
<% entry, trade, account = @entry, @entry.account_trade, @entry.account %>
|
||||
<%= drawer(reload_on_close: true) do %>
|
||||
<%= render "account/trades/header", entry: @entry %>
|
||||
|
||||
<%= 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 -entry.amount_money %>
|
||||
</span>
|
||||
|
||||
<span class="text-lg text-gray-500">
|
||||
<%= entry.currency %>
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<span class="text-sm text-gray-500">
|
||||
<%= I18n.l(entry.date, format: :long) %>
|
||||
</span>
|
||||
</header>
|
||||
<% trade = @entry.account_trade %>
|
||||
|
||||
<div class="space-y-2">
|
||||
<!-- Overview Section -->
|
||||
<%= disclosure t(".overview") do %>
|
||||
<div class="pb-4">
|
||||
<dl class="space-y-3 px-3 py-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-gray-500"><%= t(".symbol_label") %></dt>
|
||||
<dd class="text-gray-900"><%= trade.security.ticker %></dd>
|
||||
</div>
|
||||
|
||||
<% if trade.buy? %>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-gray-500"><%= t(".purchase_qty_label") %></dt>
|
||||
<dd class="text-gray-900"><%= trade.qty.abs %></dd>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-gray-500"><%= t(".purchase_price_label") %></dt>
|
||||
<dd class="text-gray-900"><%= format_money trade.price_money %></dd>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if trade.security.current_price.present? %>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-gray-500"><%= t(".current_market_price_label") %></dt>
|
||||
<dd class="text-gray-900"><%= format_money trade.security.current_price %></dd>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if trade.buy? && trade.unrealized_gain_loss.present? %>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<dt class="text-gray-500"><%= t(".total_return_label") %></dt>
|
||||
<dd style="color: <%= trade.unrealized_gain_loss.color %>;">
|
||||
<%= render "shared/trend_change", trend: trade.unrealized_gain_loss %>
|
||||
</dd>
|
||||
</div>
|
||||
<% end %>
|
||||
</dl>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Details Section -->
|
||||
<%= disclosure t(".details") do %>
|
||||
<div class="pb-4">
|
||||
<%= styled_form_with model: [account, entry],
|
||||
url: account_trade_path(account, entry),
|
||||
<%= styled_form_with model: @entry,
|
||||
url: account_trade_path(@entry),
|
||||
class: "space-y-2",
|
||||
data: { controller: "auto-submit-form" } do |f| %>
|
||||
<%= f.date_field :date,
|
||||
label: t(".date_label"),
|
||||
max: Date.current,
|
||||
max: Date.today,
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
|
||||
<%= f.fields_for :entryable do |ef| %>
|
||||
<%= ef.number_field :qty,
|
||||
<div class="flex items-center gap-2">
|
||||
<%= f.select :nature,
|
||||
[["Buy", "outflow"], ["Sell", "inflow"]],
|
||||
{ container_class: "w-1/3", label: "Type", selected: @entry.amount.negative? ? "inflow" : "outflow" },
|
||||
{ data: { "auto-submit-form-target": "auto" } } %>
|
||||
|
||||
<%= f.fields_for :entryable do |ef| %>
|
||||
<%= ef.number_field :qty,
|
||||
label: t(".quantity_label"),
|
||||
step: "any",
|
||||
value: trade.qty.abs,
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= f.fields_for :entryable do |ef| %>
|
||||
<%= ef.money_field :price,
|
||||
label: t(".cost_per_share_label"),
|
||||
disable_currency: true,
|
||||
|
@ -91,8 +45,8 @@
|
|||
<!-- Additional Section -->
|
||||
<%= disclosure t(".additional") do %>
|
||||
<div class="pb-4">
|
||||
<%= styled_form_with model: [account, entry],
|
||||
url: account_trade_path(account, entry),
|
||||
<%= styled_form_with model: @entry,
|
||||
url: account_trade_path(@entry),
|
||||
class: "space-y-2",
|
||||
data: { controller: "auto-submit-form" } do |f| %>
|
||||
<%= f.text_area :notes,
|
||||
|
@ -108,8 +62,8 @@
|
|||
<%= disclosure t(".settings") do %>
|
||||
<div class="pb-4">
|
||||
<!-- Exclude Trade Form -->
|
||||
<%= styled_form_with model: [account, entry],
|
||||
url: account_trade_path(account, entry),
|
||||
<%= styled_form_with model: @entry,
|
||||
url: account_trade_path(@entry),
|
||||
class: "p-3",
|
||||
data: { controller: "auto-submit-form" } do |f| %>
|
||||
<div class="flex cursor-pointer items-center gap-2 justify-between">
|
||||
|
@ -136,11 +90,11 @@
|
|||
</div>
|
||||
|
||||
<%= button_to t(".delete"),
|
||||
account_entry_path(account, entry),
|
||||
account_entry_path(@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" } %>
|
||||
data: { turbo_confirm: true } %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue