mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-05 21:45:23 +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,5 +1,5 @@
|
|||
<%# locals: (entry:, selectable: true, show_balance: false, origin: nil) %>
|
||||
<%# locals: (entry:, selectable: true, show_balance: false) %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(entry) do %>
|
||||
<%= render partial: entry.entryable.to_partial_path, locals: { entry:, selectable:, show_balance:, origin: } %>
|
||||
<%= render partial: entry.entryable.to_partial_path, locals: { entry:, selectable:, show_balance: } %>
|
||||
<% end %>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -9,13 +9,13 @@
|
|||
<%= 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 %>
|
||||
<%= link_to new_account_valuation_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("circle-dollar-sign", class: "text-gray-500 w-5 h-5") %>
|
||||
<%= tag.span t(".new_balance"), class: "text-sm" %>
|
||||
<% end %>
|
||||
|
||||
<% unless @account.crypto? %>
|
||||
<%= 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 %>
|
||||
<%= link_to @account.investment? ? new_account_trade_path(account_id: @account.id) : new_account_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 %>
|
||||
|
@ -75,7 +75,7 @@
|
|||
|
||||
<div class="space-y-4">
|
||||
<%= entries_by_date(@entries) do |entries| %>
|
||||
<%= render entries, show_balance: true, origin: "account" %>
|
||||
<%= render entries, show_balance: true %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<%= image_tag "https://logo.synthfinance.com/ticker/#{holding.ticker}", class: "w-9 h-9 rounded-full" %>
|
||||
|
||||
<div class="space-y-0.5">
|
||||
<%= link_to holding.name, account_holding_path(holding.account, holding), data: { turbo_frame: :drawer }, class: "hover:underline" %>
|
||||
<%= link_to holding.name, account_holding_path(holding), data: { turbo_frame: :drawer }, class: "hover:underline" %>
|
||||
|
||||
<% if holding.amount %>
|
||||
<%= tag.p holding.ticker, class: "text-gray-500 text-xs uppercase" %>
|
||||
|
|
|
@ -101,10 +101,10 @@
|
|||
</div>
|
||||
|
||||
<%= button_to t(".delete"),
|
||||
account_holding_path(@holding.account, @holding),
|
||||
account_holding_path(@holding),
|
||||
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>
|
||||
</details>
|
||||
|
|
|
@ -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 %>
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
<%= styled_form_with model: @entry, url: transactions_path, class: "space-y-4", data: { turbo_frame: "_top" } do |f| %>
|
||||
<%= styled_form_with model: @entry, url: account_transactions_path, class: "space-y-4" do |f| %>
|
||||
|
||||
<% if entry.errors.any? %>
|
||||
<%= render "shared/form_errors", model: entry %>
|
||||
<% end %>
|
||||
|
||||
<section>
|
||||
<fieldset class="bg-gray-50 rounded-lg p-1 grid grid-flow-col justify-stretch gap-x-2">
|
||||
<%= radio_tab_tag form: f, name: :nature, value: :expense, label: t(".expense"), icon: "minus-circle", checked: params[:nature] == "expense" || params[:nature].nil? %>
|
||||
<%= radio_tab_tag form: f, name: :nature, value: :income, label: t(".income"), icon: "plus-circle", checked: params[:nature] == "income" %>
|
||||
<%= radio_tab_tag form: f, name: :nature, value: :outflow, label: t(".expense"), icon: "minus-circle", checked: params[:nature] == "outflow" || params[:nature].nil? %>
|
||||
<%= radio_tab_tag form: f, name: :nature, value: :inflow, label: t(".income"), icon: "plus-circle", checked: params[:nature] == "inflow" %>
|
||||
<%= link_to new_account_transfer_path, data: { turbo_frame: :modal }, 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 %>
|
||||
<%= lucide_icon "arrow-right-left", class: "w-5 h-5" %>
|
||||
<%= tag.span t(".transfer") %>
|
||||
|
@ -12,9 +17,14 @@
|
|||
|
||||
<section class="space-y-2 overflow-hidden">
|
||||
<%= f.text_field :name, label: t(".description"), placeholder: t(".description_placeholder"), required: true %>
|
||||
<%= f.collection_select :account_id, Current.family.accounts.manual.alphabetically, :id, :name, { prompt: t(".account_prompt"), label: t(".account") }, required: true, class: "form-field__input text-ellipsis" %>
|
||||
|
||||
<% if @entry.account_id %>
|
||||
<%= f.hidden_field :account_id %>
|
||||
<% else %>
|
||||
<%= f.collection_select :account_id, Current.family.accounts.manual.alphabetically, :id, :name, { prompt: t(".account_prompt"), label: t(".account") }, required: true, class: "form-field__input text-ellipsis" %>
|
||||
<% end %>
|
||||
|
||||
<%= f.money_field :amount, label: t(".amount"), required: true %>
|
||||
<%= f.hidden_field :entryable_type, value: "Account::Transaction" %>
|
||||
<%= f.fields_for :entryable do |ef| %>
|
||||
<%= ef.collection_select :category_id, Current.family.categories.alphabetically, :id, :name, { prompt: t(".category_prompt"), label: t(".category") } %>
|
||||
<% end %>
|
23
app/views/account/transactions/_header.html.erb
Normal file
23
app/views/account/transactions/_header.html.erb
Normal file
|
@ -0,0 +1,23 @@
|
|||
<%# locals: (entry:) %>
|
||||
|
||||
<%= tag.header class: "mb-4 space-y-1", id: dom_id(entry, :header) do %>
|
||||
<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>
|
||||
|
||||
<% if entry.marked_as_transfer? %>
|
||||
<%= lucide_icon "arrow-left-right", class: "text-gray-500 mt-1 w-5 h-5" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<span class="text-sm text-gray-500">
|
||||
<%= I18n.l(entry.date, format: :long) %>
|
||||
</span>
|
||||
<% end %>
|
|
@ -8,7 +8,7 @@
|
|||
<div class="flex items-center gap-1 text-gray-500">
|
||||
<%= turbo_frame_tag "bulk_transaction_edit_drawer" %>
|
||||
|
||||
<%= form_with url: mark_transfers_transactions_path,
|
||||
<%= form_with url: mark_transfers_account_transactions_path,
|
||||
scope: "bulk_update",
|
||||
data: {
|
||||
turbo_frame: "_top",
|
||||
|
@ -28,14 +28,14 @@
|
|||
</button>
|
||||
<% end %>
|
||||
|
||||
<%= link_to bulk_edit_transactions_path,
|
||||
<%= link_to bulk_edit_account_transactions_path,
|
||||
class: "p-1.5 group hover:bg-gray-700 flex items-center justify-center rounded-md",
|
||||
title: "Edit",
|
||||
data: { turbo_frame: "bulk_transaction_edit_drawer" } do %>
|
||||
<%= lucide_icon "pencil-line", class: "w-5 group-hover:text-white" %>
|
||||
<% end %>
|
||||
|
||||
<%= 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,4 +1,4 @@
|
|||
<%# locals: (entry:, selectable: true, show_balance: false, origin: nil) %>
|
||||
<%# locals: (entry:, selectable: true, show_balance: false) %>
|
||||
<% transaction, account = entry.account_transaction, entry.account %>
|
||||
|
||||
<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">
|
||||
|
@ -20,7 +20,7 @@
|
|||
<%= content_tag :p, transaction.name %>
|
||||
<% else %>
|
||||
<%= link_to transaction.name,
|
||||
entry.transfer.present? ? account_transfer_path(entry.transfer, origin:) : account_entry_path(account, entry, origin:),
|
||||
entry.transfer.present? ? account_transfer_path(entry.transfer) : account_entry_path(entry),
|
||||
data: { turbo_frame: "drawer", turbo_prefetch: false },
|
||||
class: "hover:underline hover:text-gray-800" %>
|
||||
<% end %>
|
||||
|
@ -43,7 +43,7 @@
|
|||
</div>
|
||||
<% else %>
|
||||
<div class="flex items-center gap-1 col-span-2">
|
||||
<%= render "categories/menu", transaction: transaction, origin: origin %>
|
||||
<%= render "categories/menu", transaction: transaction %>
|
||||
</div>
|
||||
|
||||
<% unless show_balance %>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<%= turbo_frame_tag "bulk_transaction_edit_drawer" do %>
|
||||
<dialog data-controller="modal"
|
||||
data-action="click->modal#clickOutside"
|
||||
class="bg-white border border-alpha-black-25 rounded-2xl max-h-[calc(100vh-32px)] max-w-[480px] w-full shadow-xs h-full mt-4 mr-4">
|
||||
<%= styled_form_with url: bulk_update_transactions_path, scope: "bulk_update", class: "h-full", data: { turbo_frame: "_top" } do |form| %>
|
||||
class="bg-white border border-alpha-black-25 rounded-2xl max-h-[calc(100vh-32px)] max-w-[480px] w-full shadow-xs h-full mt-4 mr-4">
|
||||
<%= styled_form_with url: bulk_update_account_transactions_path, scope: "bulk_update", class: "h-full", data: { turbo_frame: "_top" } do |form| %>
|
||||
<div class="flex h-full flex-col justify-between p-4">
|
||||
<div>
|
||||
<div class="flex h-9 items-center justify-end">
|
|
@ -2,7 +2,7 @@
|
|||
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="font-medium text-lg"><%= t(".transactions") %></h3>
|
||||
<%= link_to new_transaction_path(account_id: @account),
|
||||
<%= link_to new_account_transaction_path(account_id: @account),
|
||||
class: "flex gap-1 font-medium items-center bg-gray-50 text-gray-900 p-2 rounded-lg",
|
||||
data: { turbo_frame: :modal } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-900") %>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<%= modal_form_wrapper title: t(".new_transaction") do %>
|
||||
<%= render "form", transaction: @transaction, entry: @entry %>
|
||||
<%= render "form", entry: @entry %>
|
||||
<% end %>
|
|
@ -1,39 +1,14 @@
|
|||
<% 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">
|
||||
<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>
|
||||
|
||||
<% if entry.marked_as_transfer? %>
|
||||
<%= lucide_icon "arrow-left-right", class: "text-gray-500 mt-1 w-5 h-5" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<span class="text-sm text-gray-500">
|
||||
<%= I18n.l(entry.date, format: :long) %>
|
||||
</span>
|
||||
</header>
|
||||
<%= drawer(reload_on_close: true) do %>
|
||||
<%= render "account/transactions/header", entry: @entry %>
|
||||
|
||||
<div class="space-y-2">
|
||||
<!-- Overview Section -->
|
||||
<%= disclosure t(".overview") do %>
|
||||
<div class="pb-4">
|
||||
<%= styled_form_with model: [account, entry],
|
||||
url: account_transaction_path(account, entry),
|
||||
<%= styled_form_with model: @entry,
|
||||
url: account_transaction_path(@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" %>
|
||||
|
@ -43,25 +18,25 @@
|
|||
max: Date.current,
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
|
||||
<% unless entry.marked_as_transfer? %>
|
||||
<% unless @entry.marked_as_transfer? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= f.select :nature,
|
||||
[["Expense", "expense"], ["Income", "income"]],
|
||||
{ container_class: "w-1/3", label: t(".nature"), selected: entry.amount.negative? ? "income" : "expense" },
|
||||
[["Expense", "outflow"], ["Income", "inflow"]],
|
||||
{ container_class: "w-1/3", label: t(".nature"), selected: @entry.amount.negative? ? "inflow" : "outflow" },
|
||||
{ data: { "auto-submit-form-target": "auto" } } %>
|
||||
|
||||
<%= f.money_field :amount, label: t(".amount"),
|
||||
container_class: "w-2/3",
|
||||
auto_submit: true,
|
||||
min: 0,
|
||||
value: entry.amount.abs %>
|
||||
value: @entry.amount.abs %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= f.select :account,
|
||||
options_for_select(
|
||||
Current.family.accounts.alphabetically.pluck(:name, :id),
|
||||
entry.account_id
|
||||
@entry.account_id
|
||||
),
|
||||
{ label: t(".account_label") },
|
||||
{ disabled: true } %>
|
||||
|
@ -72,55 +47,45 @@
|
|||
<!-- Details Section -->
|
||||
<%= disclosure t(".details") do %>
|
||||
<div class="pb-4">
|
||||
<%= styled_form_with model: [account, entry],
|
||||
url: account_transaction_path(account, entry),
|
||||
<%= styled_form_with model: @entry,
|
||||
url: account_transaction_path(@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? %>
|
||||
<% unless @entry.marked_as_transfer? %>
|
||||
<%= f.fields_for :entryable do |ef| %>
|
||||
<%= ef.collection_select :category_id,
|
||||
Current.family.categories.alphabetically,
|
||||
Current.family.categories.alphabetically,
|
||||
:id, :name,
|
||||
{ prompt: t(".category_placeholder"),
|
||||
label: t(".category_label"),
|
||||
class: "text-gray-400" },
|
||||
{ label: t(".category_label"),
|
||||
class: "text-gray-400", include_blank: t(".uncategorized") },
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
|
||||
<%= ef.collection_select :merchant_id,
|
||||
Current.family.merchants.alphabetically,
|
||||
Current.family.merchants.alphabetically,
|
||||
:id, :name,
|
||||
{ prompt: t(".merchant_placeholder"),
|
||||
{ include_blank: t(".none"),
|
||||
label: t(".merchant_label"),
|
||||
class: "text-gray-400" },
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
<% end %>
|
||||
|
||||
<%= ef.select :tag_ids,
|
||||
options_for_select(
|
||||
<%= ef.select :tag_ids,
|
||||
Current.family.tags.alphabetically.pluck(:name, :id),
|
||||
transaction.tag_ids
|
||||
),
|
||||
{
|
||||
multiple: true,
|
||||
label: t(".tags_label"),
|
||||
container_class: "h-40"
|
||||
},
|
||||
{
|
||||
include_blank: t(".none"),
|
||||
multiple: true,
|
||||
label: t(".tags_label"),
|
||||
container_class: "h-40"
|
||||
},
|
||||
{ "data-auto-submit-form-target": "auto" } %>
|
||||
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= styled_form_with model: [account, entry],
|
||||
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,
|
||||
<%= f.text_area :notes,
|
||||
label: t(".note_label"),
|
||||
placeholder: t(".note_placeholder"),
|
||||
rows: 5,
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
<% end %>
|
||||
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
@ -129,11 +94,10 @@
|
|||
<%= disclosure t(".settings") do %>
|
||||
<div class="pb-4">
|
||||
<!-- Exclude Transaction Form -->
|
||||
<%= styled_form_with model: [account, entry],
|
||||
url: account_transaction_path(account, entry),
|
||||
<%= styled_form_with model: @entry,
|
||||
url: account_transaction_path(@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>
|
||||
|
@ -158,7 +122,7 @@
|
|||
</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",
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
|
||||
<section>
|
||||
<fieldset class="bg-gray-50 rounded-lg p-1 grid grid-flow-col justify-stretch gap-x-2">
|
||||
<%= link_to new_transaction_path(nature: "expense"), data: { turbo_frame: :modal }, class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-gray-400" do %>
|
||||
<%= link_to new_account_transaction_path(nature: "expense"), data: { turbo_frame: :modal }, class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-gray-400" do %>
|
||||
<%= lucide_icon "minus-circle", class: "w-5 h-5" %>
|
||||
<%= tag.span t(".expense") %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_transaction_path(nature: "income"), data: { turbo_frame: :modal }, class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-gray-400" do %>
|
||||
<%= link_to new_account_transaction_path(nature: "income"), data: { turbo_frame: :modal }, class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-gray-400" do %>
|
||||
<%= lucide_icon "plus-circle", class: "w-5 h-5" %>
|
||||
<%= tag.span t(".income") %>
|
||||
<% end %>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<%# locals: (entry:) %>
|
||||
|
||||
<%= form_with url: unmark_transfers_transactions_path, class: "flex items-center", data: {
|
||||
<%= form_with url: unmark_transfers_account_transactions_path, class: "flex items-center", data: {
|
||||
turbo_confirm: {
|
||||
title: t(".remove_transfer"),
|
||||
body: t(".remove_transfer_body"),
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
<%# locals: (entry:) %>
|
||||
|
||||
<%= 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| %>
|
||||
<%= styled_form_with model: entry, url: account_valuations_path, class: "space-y-4" do |form| %>
|
||||
<%= form.hidden_field :account_id %>
|
||||
|
||||
<% if entry.errors.any? %>
|
||||
<%= render "shared/form_errors", model: entry %>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-3">
|
||||
<%= form.date_field :date, label: true, required: true, value: Date.today, min: Account::Entry.min_supported_date, max: Date.today %>
|
||||
<%= form.money_field :amount, label: t(".amount"), required: true %>
|
||||
|
|
19
app/views/account/valuations/_header.html.erb
Normal file
19
app/views/account/valuations/_header.html.erb
Normal file
|
@ -0,0 +1,19 @@
|
|||
<%# locals: (entry:) %>
|
||||
|
||||
<%= tag.header class: "mb-4 space-y-1", id: dom_id(entry, :header) do %>
|
||||
<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>
|
||||
<% end %>
|
|
@ -1,4 +1,4 @@
|
|||
<%# locals: (entry:, selectable: true, show_balance: false, origin: nil) %>
|
||||
<%# locals: (entry:, selectable: true, show_balance: false) %>
|
||||
|
||||
<% account = entry.account %>
|
||||
<% valuation = entry.account_valuation %>
|
||||
|
@ -21,7 +21,7 @@
|
|||
<%= content_tag :p, entry.name %>
|
||||
<% else %>
|
||||
<%= link_to valuation.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 %>
|
||||
|
|
|
@ -1,30 +1,14 @@
|
|||
<% entry, account = @entry, @entry.account %>
|
||||
|
||||
<%= 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>
|
||||
<%= drawer(reload_on_close: true) do %>
|
||||
<%= render "account/valuations/header", entry: %>
|
||||
|
||||
<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),
|
||||
<%= styled_form_with model: entry,
|
||||
url: account_entry_path(entry),
|
||||
class: "space-y-2",
|
||||
data: { controller: "auto-submit-form" } do |f| %>
|
||||
<%= f.text_field :name,
|
||||
|
@ -48,8 +32,8 @@
|
|||
<!-- Details Section -->
|
||||
<%= disclosure t(".details") do %>
|
||||
<div class="pb-4">
|
||||
<%= styled_form_with model: [account, entry],
|
||||
url: account_entry_path(account, entry),
|
||||
<%= styled_form_with model: entry,
|
||||
url: account_entry_path(entry),
|
||||
class: "space-y-2",
|
||||
data: { controller: "auto-submit-form" } do |f| %>
|
||||
<%= f.text_area :notes,
|
||||
|
@ -72,7 +56,7 @@
|
|||
</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" } %>
|
||||
|
|
5
app/views/accounts/_chart_loader.html.erb
Normal file
5
app/views/accounts/_chart_loader.html.erb
Normal file
|
@ -0,0 +1,5 @@
|
|||
<div class="h-10">
|
||||
</div>
|
||||
<div class="h-64 flex items-center justify-center">
|
||||
<p class="text-gray-500 animate-pulse text-sm">Loading...</p>
|
||||
</div>
|
32
app/views/accounts/chart.html.erb
Normal file
32
app/views/accounts/chart.html.erb
Normal file
|
@ -0,0 +1,32 @@
|
|||
<% period = Period.from_param(params[:period]) %>
|
||||
<% series = @account.series(period: period) %>
|
||||
<% trend = series.trend %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(@account, :chart_details) do %>
|
||||
<div class="px-4">
|
||||
<% if trend.direction.flat? %>
|
||||
<%= tag.span t(".no_change"), class: "text-gray-500" %>
|
||||
<% else %>
|
||||
<%= tag.span "#{trend.value.positive? ? "+" : ""}#{format_money(trend.value)}", style: "color: #{trend.color}" %>
|
||||
<% unless trend.percent.infinite? %>
|
||||
<%= tag.span "(#{trend.percent}%)", style: "color: #{trend.color}" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= tag.span period_label(period), class: "text-gray-500" %>
|
||||
</div>
|
||||
|
||||
<div class="h-64">
|
||||
<% if series %>
|
||||
<div
|
||||
id="lineChart"
|
||||
class="w-full h-full"
|
||||
data-controller="time-series-chart"
|
||||
data-time-series-chart-data-value="<%= series.to_json %>"></div>
|
||||
<% else %>
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<p class="text-gray-500">No data available for the selected period.</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
|
@ -1,5 +1,5 @@
|
|||
<%# locals: (account:) %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(account, :entries), src: account_entries_path(account) do %>
|
||||
<%= turbo_frame_tag dom_id(account, :entries), src: account_entries_path(account_id: account.id) do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% end %>
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
<%# locals: (account:, title: nil, tooltip: nil, **args) %>
|
||||
|
||||
<% period = Period.from_param(params[:period]) %>
|
||||
<% series = account.series(period: period) %>
|
||||
<% trend = series.trend %>
|
||||
<% default_value_title = account.asset? ? t(".balance") : t(".owed") %>
|
||||
|
||||
<div id="<%= dom_id(account, :chart) %>" class="bg-white shadow-xs rounded-xl border border-alpha-black-25 rounded-lg">
|
||||
<div class="p-4 flex justify-between">
|
||||
<div id="<%= dom_id(account, :chart) %>" class="bg-white shadow-xs rounded-xl border border-alpha-black-25 rounded-lg space-y-2">
|
||||
<div class="flex justify-between px-4 pt-4 mb-2">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-1">
|
||||
<%= tag.p title || default_value_title, class: "text-sm font-medium text-gray-500" %>
|
||||
|
@ -14,19 +12,6 @@
|
|||
</div>
|
||||
|
||||
<%= tag.p format_money(account.value), class: "text-gray-900 text-3xl font-medium" %>
|
||||
|
||||
<div>
|
||||
<% if trend.direction.flat? %>
|
||||
<%= tag.span t(".no_change"), class: "text-gray-500" %>
|
||||
<% else %>
|
||||
<%= tag.span "#{trend.value.positive? ? "+" : ""}#{format_money(trend.value)}", style: "color: #{trend.color}" %>
|
||||
<% unless trend.percent.infinite? %>
|
||||
<%= tag.span "(#{trend.percent}%)", style: "color: #{trend.color}" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= tag.span period_label(period), class: "text-gray-500" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= form_with url: request.path, method: :get, data: { controller: "auto-submit-form" } do |form| %>
|
||||
|
@ -34,7 +19,7 @@
|
|||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="h-64 flex items-center justify-center text-2xl font-bold">
|
||||
<%= render "shared/line_chart", series: series %>
|
||||
</div>
|
||||
<%= turbo_frame_tag dom_id(account, :chart_details), src: chart_account_path(account, period: period.name) do %>
|
||||
<%= render "accounts/chart_loader" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
<%= link_to key.titleize,
|
||||
account_path(account, tab: key),
|
||||
data: { turbo: false },
|
||||
class: [
|
||||
"px-2 py-1.5 rounded-md border border-transparent",
|
||||
"bg-white shadow-xs border-alpha-black-50": is_selected
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<%# locals: (transaction:, origin: nil) %>
|
||||
<div class="relative" data-controller="menu">
|
||||
<%# locals: (transaction:) %>
|
||||
|
||||
<div class="relative" data-controller="menu" id="<%= dom_id(transaction, :category_menu) %>">
|
||||
<button data-menu-target="button" class="flex cursor-pointer">
|
||||
<%= render partial: "categories/badge", locals: { category: transaction.category } %>
|
||||
</button>
|
||||
<div data-menu-target="content" class="absolute z-10 hidden w-screen mt-2 max-w-min cursor-default">
|
||||
<div class="w-64 text-sm font-semibold leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= turbo_frame_tag "category_dropdown", src: category_dropdown_path(category_id: transaction.category_id, transaction_id: transaction.id, origin: origin), loading: :lazy do %>
|
||||
<%= turbo_frame_tag "category_dropdown", src: category_dropdown_path(category_id: transaction.category_id, transaction_id: transaction.id), loading: :lazy do %>
|
||||
<div class="p-6 flex items-center justify-center">
|
||||
<p class="text-sm text-gray-500 animate-pulse"><%= t(".loading") %></p>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
<%# locals: (category:, origin: nil) %>
|
||||
<%# locals: (category:) %>
|
||||
<% is_selected = category.id === @selected_category&.id %>
|
||||
|
||||
<%= content_tag :div, class: ["filterable-item flex justify-between items-center border-none rounded-lg px-2 py-1 group w-full", { "bg-gray-25": is_selected }], data: { filter_name: category.name } do %>
|
||||
<%= button_to account_transaction_path(@transaction.entry.account, @transaction.entry, account_entry: { origin: origin,entryable_type: "Account::Transaction", entryable_attributes: { id: @transaction.id, category_id: category.id } }), method: :patch, data: { turbo_frame: dom_id(@transaction.entry) }, class: "flex w-full items-center gap-1.5 cursor-pointer" do %>
|
||||
<%= button_to account_transaction_category_path(
|
||||
@transaction.entry,
|
||||
account_entry: {
|
||||
entryable_type: "Account::Transaction",
|
||||
entryable_attributes: { id: @transaction.id, category_id: category.id }
|
||||
}
|
||||
),
|
||||
method: :patch,
|
||||
class: "flex w-full items-center gap-1.5 cursor-pointer" do %>
|
||||
|
||||
<span class="w-5 h-5">
|
||||
<%= lucide_icon("check", class: "w-5 h-5 text-gray-500") if is_selected %>
|
||||
</span>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<%= t(".no_categories") %>
|
||||
</div>
|
||||
<% @categories.each do |category| %>
|
||||
<%= render partial: "category/dropdowns/row", locals: { category:, origin: params[:origin] } %>
|
||||
<%= render partial: "category/dropdowns/row", locals: { category: } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<hr>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<%# locals: (account:) %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(account, :cash), src: account_cashes_path(account) do %>
|
||||
<%= turbo_frame_tag dom_id(account, :cash), src: account_cashes_path(account_id: account.id) do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% end %>
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
<%# locals: (account:, **args) %>
|
||||
|
||||
<% period = Period.from_param(params[:period]) %>
|
||||
<% series = account.series(period: period) %>
|
||||
<% trend = series.trend %>
|
||||
|
||||
<div id="<%= dom_id(account, :chart) %>" class="bg-white shadow-xs rounded-xl border border-alpha-black-25 rounded-lg">
|
||||
<div class="p-4 flex justify-between">
|
||||
<div class="space-y-2">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<%# locals: (account:) %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(account, :holdings), src: account_holdings_path(account) do %>
|
||||
<%= turbo_frame_tag dom_id(account, :holdings), src: account_holdings_path(account_id: account.id) do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% end %>
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
<%= render_flash_notifications %>
|
||||
|
||||
<% if Current.family&.syncing? %>
|
||||
<%= render "shared/notification", id: "syncing-notification", type: :processing, message: t(".syncing") %>
|
||||
<%= render "shared/syncing_notice" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
11
app/views/securities/_combobox_security.turbo_stream.erb
Normal file
11
app/views/securities/_combobox_security.turbo_stream.erb
Normal file
|
@ -0,0 +1,11 @@
|
|||
<div class="flex items-center">
|
||||
<%= image_tag(combobox_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">
|
||||
<%= combobox_security.name.presence || combobox_security.symbol %>
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">
|
||||
<%= "#{combobox_security.symbol} (#{combobox_security.exchange_acronym})" %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
2
app/views/securities/index.turbo_stream.erb
Normal file
2
app/views/securities/index.turbo_stream.erb
Normal file
|
@ -0,0 +1,2 @@
|
|||
<%= async_combobox_options @securities.map(&:to_combobox_option),
|
||||
render_in: { partial: "securities/combobox_security" } %>
|
|
@ -1,5 +1,10 @@
|
|||
<%# locals: (content:, reload_on_close: false) %>
|
||||
|
||||
<%= turbo_frame_tag "drawer" do %>
|
||||
<dialog class="bg-white border border-alpha-black-25 rounded-2xl max-w-[480px] w-full shadow-xs h-full mt-4 mr-4 focus-visible:outline-none" data-controller="modal" data-action="click->modal#clickOutside">
|
||||
<dialog class="bg-white border border-alpha-black-25 rounded-2xl max-w-[480px] w-full shadow-xs h-full mt-4 mr-4 focus-visible:outline-none"
|
||||
data-controller="modal"
|
||||
data-action="click->modal#clickOutside"
|
||||
data-modal-reload-on-close-value="<%= reload_on_close %>">
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex justify-end items-center p-4">
|
||||
<div data-action="click->modal#close" class="cursor-pointer p-2">
|
||||
|
|
6
app/views/shared/_form_errors.html.erb
Normal file
6
app/views/shared/_form_errors.html.erb
Normal file
|
@ -0,0 +1,6 @@
|
|||
<%# locals: (model:) %>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<%= lucide_icon("alert-circle", class: "text-red-500 w-4 h-4 shrink-0") %>
|
||||
<p class="text-red-500 text-sm"><%= model.errors.full_messages.to_sentence %></p>
|
||||
</div>
|
|
@ -1,10 +1,9 @@
|
|||
<%# locals: (message:, type: "notice", id: nil, **_opts) %>
|
||||
<%# locals: (message:, type: "notice", **_opts) %>
|
||||
|
||||
<% type = type.to_sym %>
|
||||
<% action = "animationend->element-removal#remove" if type == :notice %>
|
||||
|
||||
<%= tag.div class: "flex gap-3 rounded-lg border bg-white p-4 group max-w-80 shadow-xs border-alpha-black-25",
|
||||
id: type == :processing ? "syncing-notification" : id,
|
||||
data: {
|
||||
controller: "element-removal",
|
||||
action: action
|
||||
|
@ -20,8 +19,6 @@
|
|||
<div class="flex h-full items-center justify-center rounded-full bg-error">
|
||||
<%= lucide_icon "x", class: "w-3 h-3" %>
|
||||
</div>
|
||||
<% when :processing %>
|
||||
<%= lucide_icon "loader", class: "w-5 h-5 text-gray-500 animate-pulse" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
|
|
7
app/views/shared/_syncing_notice.html.erb
Normal file
7
app/views/shared/_syncing_notice.html.erb
Normal file
|
@ -0,0 +1,7 @@
|
|||
<%= tag.div id: "syncing-notice", class: "flex gap-3 rounded-lg border bg-white p-4 group max-w-80 shadow-xs border-alpha-black-25" do %>
|
||||
<div class="h-5 w-5 shrink-0 p-px text-white">
|
||||
<%= lucide_icon "loader", class: "w-5 h-5 text-gray-500 animate-pulse" %>
|
||||
</div>
|
||||
|
||||
<%= tag.p t(".syncing"), class: "text-gray-900 text-sm font-medium" %>
|
||||
<% end %>
|
|
@ -16,7 +16,7 @@
|
|||
<p class="text-sm font-medium text-gray-900"><%= t(".import") %></p>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_transaction_path, class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2", data: { turbo_frame: :modal } do %>
|
||||
<%= link_to new_account_transaction_path, class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2", data: { turbo_frame: :modal } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<p class="text-sm font-medium">New transaction</p>
|
||||
<% end %>
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
</div>
|
||||
<div class="space-y-6">
|
||||
<%= entries_by_date(@transaction_entries, totals: true) do |entries| %>
|
||||
<%= render entries, origin: "transactions" %>
|
||||
<%= render entries %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
<% content_for :sidebar do %>
|
||||
<%= render "settings/nav" %>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-4">
|
||||
<h1 class="text-gray-900 text-xl font-medium mb-4">Rules</h1>
|
||||
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
|
||||
<div class="flex justify-center items-center py-20">
|
||||
<p class="text-gray-500">Transaction rules coming soon...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<%= previous_setting("Merchants", merchants_path) %>
|
||||
<%= next_setting("Imports", imports_path) %>
|
||||
</div>
|
||||
</div>
|
Loading…
Add table
Add a link
Reference in a new issue