mirror of
https://github.com/we-promise/sure.git
synced 2026-04-19 12:04:08 +00:00
Improve investment activity labels UX and add convert-to-trade feature (#649)
* Add `investment_activity_label` to trades and enhance activity label handling - Introduced `investment_activity_label` column to the `trades` table with a migration. - Backfilled existing `trades` with activity labels based on quantity (`Buy`, `Sell`, or `Other`). - Replaced `category_id` in trades with `investment_activity_label` for better alignment with transaction labels. - Updated views and controllers to display and manage activity labels for trades. - Added localized badge components for displaying and editing labels dynamically. - Enhanced `PlaidAccount::Investments::TransactionsProcessor` to assign and process activity labels automatically. - Added investment flows section to reports for tracking contributions and withdrawals. - Refactored related tests and models for consistency and to ensure proper validation and filtering. * Improve handling of `investment_activity_label`, trade type, and security selection in trades and transactions - Refined label assignment logic in `trades_controller` to default to `Buy`/`Sell` based on transaction nature. - Simplified security selection in `transactions_controller` by resolving via unique IDs or custom tickers. - Streamlined UI for trade and transaction forms by updating dropdown options and label text. - Enabled quick-edit badges to open `convert_to_trade` modal when applicable, enhancing flexibility. - Adjusted tests and views to align with updated workflows and ensure consistent behavior. * Improve handling of `investment_activity_label`, trade type, and security selection in trades and transactions - Refined label assignment logic in `trades_controller` to default to `Buy`/`Sell` based on transaction nature. - Simplified security selection in `transactions_controller` by resolving via unique IDs or custom tickers. - Streamlined UI for trade and transaction forms by updating dropdown options and label text. - Enabled quick-edit badges to open `convert_to_trade` modal when applicable, enhancing flexibility. - Adjusted tests and views to align with updated workflows and ensure consistent behavior. * Improve handling of `investment_activity_label`, trade type, and security selection in trades and transactions - Refined label assignment logic in `trades_controller` to default to `Buy`/`Sell` based on transaction nature. - Simplified security selection in `transactions_controller` by resolving via unique IDs or custom tickers. - Streamlined UI for trade and transaction forms by updating dropdown options and label text. - Enabled quick-edit badges to open `convert_to_trade` modal when applicable, enhancing flexibility. - Adjusted tests and views to align with updated workflows and ensure consistent behavior. * Add safeguard for `dropdownTarget` existence in quick edit controller - Prevent errors by ensuring `dropdownTarget` is present before toggling its visibility. * Fix undefined method 'category' for Trade on mobile view Trade model uses investment_activity_label, not category. The upstream merge introduced a call to trade.category which doesn't exist. Use the activity label badge on mobile instead. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix activity label logic for zero/blank quantity and sell inference - Return `nil` for blank or zero quantity in `investment_activity_label_for`. - Correct `is_sell` logic to use the amount’s sign properly in `transactions_controller`. * Fix i18n key paths in transactions controller for convert_to_trade - Update flash message translations to use full i18n paths. - Use `BigDecimal` for quantity and price calculations to improve precision. --------- Co-authored-by: Josh Waldrep <joshua.waldrep5+github@gmail.com> Co-authored-by: luckyPipewrench <luckypipewrench@proton.me> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
33
app/views/investment_activity/_badge.html.erb
Normal file
33
app/views/investment_activity/_badge.html.erb
Normal file
@@ -0,0 +1,33 @@
|
||||
<%# locals: (label:) %>
|
||||
<%# Simple non-interactive badge for displaying activity labels %>
|
||||
<%
|
||||
# Color mapping for different investment activity labels
|
||||
color = case label
|
||||
when "Buy"
|
||||
"rgb(59 130 246)" # blue
|
||||
when "Sell"
|
||||
"rgb(239 68 68)" # red
|
||||
when "Dividend", "Interest"
|
||||
"rgb(34 197 94)" # green
|
||||
when "Contribution"
|
||||
"rgb(168 85 247)" # purple
|
||||
when "Withdrawal"
|
||||
"rgb(249 115 22)" # orange
|
||||
when "Fee"
|
||||
"rgb(107 114 128)" # gray
|
||||
when "Transfer", "Sweep In", "Sweep Out", "Exchange"
|
||||
"rgb(107 114 128)" # gray
|
||||
when "Reinvestment"
|
||||
"rgb(59 130 246)" # blue
|
||||
else
|
||||
"rgb(107 114 128)" # gray for "Other"
|
||||
end
|
||||
%>
|
||||
|
||||
<span class="inline-flex items-center gap-1 text-xs font-medium rounded px-1.5 py-0.5 border"
|
||||
style="
|
||||
background-color: color-mix(in oklab, <%= color %> 10%, transparent);
|
||||
border-color: color-mix(in oklab, <%= color %> 20%, transparent);
|
||||
color: <%= color %>;">
|
||||
<%= label %>
|
||||
</span>
|
||||
95
app/views/investment_activity/_quick_edit_badge.html.erb
Normal file
95
app/views/investment_activity/_quick_edit_badge.html.erb
Normal file
@@ -0,0 +1,95 @@
|
||||
<%# locals: (entry:, entryable:) %>
|
||||
<%
|
||||
label = entryable.investment_activity_label
|
||||
has_label = label.present?
|
||||
|
||||
# Build the correct URL based on entryable type
|
||||
update_url = entryable.is_a?(Transaction) ? transaction_path(entry) : trade_path(entry)
|
||||
|
||||
# Color mapping for different investment activity labels using design system tokens
|
||||
color = if has_label
|
||||
case label
|
||||
when "Buy"
|
||||
"var(--color-blue-500)"
|
||||
when "Sell"
|
||||
"var(--color-red-500)"
|
||||
when "Dividend", "Interest"
|
||||
"var(--color-green-500)"
|
||||
when "Contribution"
|
||||
"var(--color-violet-500)"
|
||||
when "Withdrawal"
|
||||
"var(--color-orange-500)"
|
||||
when "Fee"
|
||||
"var(--color-gray-500)"
|
||||
when "Transfer", "Sweep In", "Sweep Out", "Exchange"
|
||||
"var(--color-gray-500)"
|
||||
when "Reinvestment"
|
||||
"var(--color-blue-500)"
|
||||
else
|
||||
"var(--color-gray-500)" # for "Other"
|
||||
end
|
||||
else
|
||||
"var(--color-gray-400)" # slightly lighter for empty state
|
||||
end
|
||||
|
||||
activity_labels = entryable.is_a?(Trade) ? Trade::ACTIVITY_LABELS : Transaction::ACTIVITY_LABELS
|
||||
entryable_type = entryable.is_a?(Trade) ? "Trade" : "Transaction"
|
||||
convert_url = entryable.is_a?(Transaction) ? convert_to_trade_transaction_path(entryable) : nil
|
||||
%>
|
||||
|
||||
<div class="relative"
|
||||
data-controller="activity-label-quick-edit"
|
||||
data-activity-label-quick-edit-url-value="<%= update_url %>"
|
||||
data-activity-label-quick-edit-entryable-id-value="<%= entryable.id %>"
|
||||
data-activity-label-quick-edit-current-label-value="<%= label %>"
|
||||
data-activity-label-quick-edit-entryable-type-value="<%= entryable_type %>"
|
||||
<% if convert_url %>
|
||||
data-activity-label-quick-edit-convert-url-value="<%= convert_url %>"
|
||||
<% end %>>
|
||||
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-xs font-medium rounded-full px-2.5 py-1 cursor-pointer hover:opacity-80 transition-opacity"
|
||||
style="<% if has_label %>
|
||||
background-color: color-mix(in oklab, <%= color %> 15%, transparent);
|
||||
border: 1px solid color-mix(in oklab, <%= color %> 25%, transparent);
|
||||
color: <%= color %>;
|
||||
<% else %>
|
||||
background-color: var(--color-surface-inset);
|
||||
border: 1px solid var(--color-border-secondary);
|
||||
color: var(--color-text-secondary);
|
||||
<% end %>"
|
||||
data-action="click->activity-label-quick-edit#toggle"
|
||||
data-activity-label-quick-edit-target="badge"
|
||||
title="<%= has_label ? t("transactions.transaction.activity_type_tooltip") : t("transactions.show.activity_type") %>">
|
||||
<% if has_label %>
|
||||
<%= label %>
|
||||
<% else %>
|
||||
<%= icon "tag", size: "xs", color: "current" %>
|
||||
<span><%= t("transactions.show.activity_type") %></span>
|
||||
<% end %>
|
||||
<%= icon "chevron-down", size: "xs", color: "current" %>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<div class="hidden absolute right-0 z-50 mt-1 w-44 rounded-lg border border-primary bg-container shadow-lg"
|
||||
data-activity-label-quick-edit-target="dropdown">
|
||||
<div class="py-1 max-h-64 overflow-y-auto">
|
||||
<% if has_label %>
|
||||
<button type="button"
|
||||
class="w-full text-left px-3 py-1.5 text-sm text-secondary hover:bg-surface-inset transition-colors border-b border-primary"
|
||||
data-action="click->activity-label-quick-edit#select"
|
||||
data-label="">
|
||||
<span class="italic"><%= t("transactions.form.none") %></span>
|
||||
</button>
|
||||
<% end %>
|
||||
<% activity_labels.each do |activity_label| %>
|
||||
<button type="button"
|
||||
class="w-full text-left px-3 py-1.5 text-sm text-primary hover:bg-surface-inset transition-colors <%= activity_label == label ? "font-semibold bg-surface-inset" : "" %>"
|
||||
data-action="click->activity-label-quick-edit#select"
|
||||
data-label="<%= activity_label %>">
|
||||
<%= activity_label %>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,21 +77,40 @@
|
||||
|
||||
<%# Investment Activity Summary %>
|
||||
<% totals = investment_statement.totals(period: period) %>
|
||||
<% if totals.trades_count > 0 %>
|
||||
<div class="px-4 pt-2">
|
||||
<p class="text-xs text-secondary uppercase font-medium mb-2"><%= t(".period_activity", period: period.label) %></p>
|
||||
<div class="flex gap-4 text-sm">
|
||||
<div>
|
||||
<span class="text-secondary"><%= t(".contributions") %>:</span>
|
||||
<span class="font-medium text-primary"><%= format_money(totals.contributions) %></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-secondary"><%= t(".withdrawals") %>:</span>
|
||||
<span class="font-medium text-primary"><%= format_money(totals.withdrawals) %></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-secondary"><%= t(".trades") %>:</span>
|
||||
<span class="font-medium text-primary"><%= totals.trades_count %></span>
|
||||
<% if totals.trades_count > 0 || totals.contributions.to_f > 0 || totals.withdrawals.to_f > 0 %>
|
||||
<div class="bg-container-inset rounded-xl p-1 mx-4">
|
||||
<div class="px-4 py-2 flex items-center uppercase text-xs font-medium text-secondary">
|
||||
<%= t(".period_activity", period: period.label) %>
|
||||
</div>
|
||||
<div class="shadow-border-xs rounded-lg bg-container font-medium text-sm">
|
||||
<div class="p-4 flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-8 h-8 rounded-full bg-green-500/10 flex items-center justify-center">
|
||||
<%= icon "trending-up", size: "sm", color: "green" %>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-secondary"><%= t(".contributions") %></p>
|
||||
<p class="font-medium text-green-600"><%= format_money(totals.contributions) %></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-8 h-8 rounded-full bg-orange-500/10 flex items-center justify-center">
|
||||
<%= icon "trending-down", size: "sm", color: "orange" %>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-secondary"><%= t(".withdrawals") %></p>
|
||||
<p class="font-medium text-orange-600"><%= format_money(totals.withdrawals) %></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-8 h-8 rounded-full bg-blue-500/10 flex items-center justify-center">
|
||||
<%= icon "arrow-left-right", size: "sm", color: "blue" %>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-secondary"><%= t(".trades") %></p>
|
||||
<p class="font-medium text-primary"><%= totals.trades_count %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
44
app/views/reports/_investment_flows.html.erb
Normal file
44
app/views/reports/_investment_flows.html.erb
Normal file
@@ -0,0 +1,44 @@
|
||||
<%# locals: (investment_flows:) %>
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm text-secondary">
|
||||
Track money flowing into and out of your investment accounts through contributions and withdrawals.
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<!-- Contributions -->
|
||||
<div class="bg-container border border-primary rounded-lg p-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<%= icon("trending-up", size: "sm", class: "text-green-600") %>
|
||||
<h3 class="text-sm font-medium text-secondary">Contributions</h3>
|
||||
</div>
|
||||
<div class="text-2xl font-semibold" style="color: rgb(34 197 94);">
|
||||
<%= format_money(investment_flows.contributions) %>
|
||||
</div>
|
||||
<p class="text-xs text-secondary mt-1">Money added to investments</p>
|
||||
</div>
|
||||
|
||||
<!-- Withdrawals -->
|
||||
<div class="bg-container border border-primary rounded-lg p-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<%= icon("trending-down", size: "sm", class: "text-orange-600") %>
|
||||
<h3 class="text-sm font-medium text-secondary">Withdrawals</h3>
|
||||
</div>
|
||||
<div class="text-2xl font-semibold" style="color: rgb(249 115 22);">
|
||||
<%= format_money(investment_flows.withdrawals) %>
|
||||
</div>
|
||||
<p class="text-xs text-secondary mt-1">Money withdrawn from investments</p>
|
||||
</div>
|
||||
|
||||
<!-- Net Flow -->
|
||||
<div class="bg-container border border-primary rounded-lg p-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<%= icon("arrow-right-left", size: "sm", class: "text-primary") %>
|
||||
<h3 class="text-sm font-medium text-secondary">Net Flow</h3>
|
||||
</div>
|
||||
<div class="text-2xl font-semibold text-primary">
|
||||
<%= format_money(investment_flows.net_flow) %>
|
||||
</div>
|
||||
<p class="text-xs text-secondary mt-1">Total net change</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex lg:hidden">
|
||||
<%= render "categories/badge_mobile", category: trade.category || trade_category %>
|
||||
<%= render "investment_activity/quick_edit_badge", entry: entry, entryable: trade %>
|
||||
</div>
|
||||
|
||||
<div class="truncate flex-shrink">
|
||||
@@ -40,8 +40,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden lg:flex">
|
||||
<%= render "categories/badge", category: trade.category || trade_category %>
|
||||
<div class="hidden lg:flex col-span-2 items-center">
|
||||
<%= render "investment_activity/quick_edit_badge", entry: entry, entryable: trade %>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0 col-span-4 lg:col-span-2 ml-auto text-right">
|
||||
|
||||
@@ -43,11 +43,6 @@
|
||||
step: "any",
|
||||
precision: 10,
|
||||
disabled: @entry.linked? %>
|
||||
|
||||
<%= ef.select :category_id,
|
||||
Current.family.categories.expenses.alphabetically.map { |c| [c.name, c.id] },
|
||||
{ include_blank: t(".no_category"), label: t(".category_label") },
|
||||
{ data: { "auto-submit-form-target": "auto" } } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -78,13 +78,6 @@
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<%# Investment activity label badge %>
|
||||
<% if transaction.investment_activity_label.present? %>
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-alpha-black-50 text-secondary" title="<%= t("transactions.transaction.activity_type_tooltip") %>">
|
||||
<%= t("transactions.activity_labels.#{transaction.investment_activity_label.parameterize(separator: '_')}") %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<%# Pending indicator %>
|
||||
<% if transaction.pending? %>
|
||||
<span class="inline-flex items-center gap-1 text-xs font-medium rounded-full px-1.5 py-0.5 border border-secondary text-secondary" title="<%= t("transactions.transaction.pending_tooltip") %>">
|
||||
@@ -144,7 +137,12 @@
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center gap-1 col-span-2">
|
||||
<%= render "transactions/transaction_category", transaction: transaction, variant: "desktop" %>
|
||||
<% if entry.account.investment? && !transaction.transfer? %>
|
||||
<%# For investment accounts, show activity label instead of category %>
|
||||
<%= render "investment_activity/quick_edit_badge", entry: entry, entryable: transaction %>
|
||||
<% else %>
|
||||
<%= render "transactions/transaction_category", transaction: transaction, variant: "desktop" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0 col-span-4 lg:col-span-2 ml-auto text-right">
|
||||
|
||||
141
app/views/transactions/convert_to_trade.html.erb
Normal file
141
app/views/transactions/convert_to_trade.html.erb
Normal file
@@ -0,0 +1,141 @@
|
||||
<%
|
||||
# Use activity_label param if provided (from quick-edit badge click),
|
||||
# otherwise determine default based on amount sign
|
||||
# Negative amount (money going out) = Buy, Positive (money coming in) = Sell
|
||||
default_label = params[:activity_label].presence || (@entry.amount > 0 ? "Sell" : "Buy")
|
||||
|
||||
# Get unique securities from account holdings for ticker suggestions
|
||||
account_securities = @entry.account.holdings
|
||||
.includes(:security)
|
||||
.where.not(security: nil)
|
||||
.map(&:security)
|
||||
.uniq(&:ticker)
|
||||
.sort_by(&:ticker)
|
||||
%>
|
||||
|
||||
<%= render DS::Dialog.new(variant: "modal", reload_on_close: true) do |dialog| %>
|
||||
<% dialog.with_header do %>
|
||||
<h2 class="text-lg font-semibold text-primary"><%= t(".title") %></h2>
|
||||
<p class="text-sm text-secondary"><%= t(".description") %></p>
|
||||
<% end %>
|
||||
|
||||
<% dialog.with_body do %>
|
||||
<%= form_with url: create_trade_from_transaction_transaction_path(@transaction), method: :post, class: "space-y-4", data: { controller: "convert-to-trade", turbo_frame: "_top" } do |f| %>
|
||||
<!-- Pre-filled Transaction Info (Read-only) -->
|
||||
<div class="space-y-2 p-3 bg-surface-inset rounded-lg border border-primary">
|
||||
<div class="text-sm">
|
||||
<span class="text-secondary"><%= t(".date_label") %></span>
|
||||
<span class="font-medium text-primary"><%= @entry.date.strftime("%b %d, %Y") %></span>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span class="text-secondary"><%= t(".account_label") %></span>
|
||||
<span class="font-medium text-primary"><%= @entry.account.name %></span>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span class="text-secondary"><%= t(".amount_label") %></span>
|
||||
<span class="font-medium <%= @entry.amount.negative? ? "text-green-600" : "text-primary" %>"><%= format_money(@entry.amount_money.abs) %></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trade-Specific Fields -->
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-1">
|
||||
<%= f.label :security_id, t(".security_label"), class: "font-medium text-sm text-primary block" %>
|
||||
<% if account_securities.any? %>
|
||||
<%= f.select :security_id,
|
||||
options_for_select(
|
||||
[[t(".security_prompt"), ""]] +
|
||||
account_securities.map { |s| ["#{s.ticker}#{s.name.present? ? " - #{s.name.truncate(30)}" : ""}", s.id] } +
|
||||
[[t(".security_custom"), "__custom__"]],
|
||||
nil
|
||||
),
|
||||
{},
|
||||
{
|
||||
class: "form-field__input border border-secondary rounded-lg px-3 py-2 w-full text-primary bg-container",
|
||||
data: {
|
||||
action: "change->convert-to-trade#toggleCustomTicker",
|
||||
convert_to_trade_target: "tickerSelect"
|
||||
}
|
||||
} %>
|
||||
<div class="hidden mt-2" data-convert-to-trade-target="customWrapper">
|
||||
<%= f.text_field :custom_ticker,
|
||||
placeholder: t(".ticker_placeholder"),
|
||||
class: "form-field__input border border-secondary rounded-lg px-3 py-2 w-full text-primary bg-container",
|
||||
pattern: "[A-Za-z0-9.:-]{1,20}",
|
||||
title: t(".ticker_hint"),
|
||||
data: { convert_to_trade_target: "customField" } %>
|
||||
</div>
|
||||
<p class="text-xs text-secondary"><%= t(".security_hint") %></p>
|
||||
<% else %>
|
||||
<%= f.text_field :ticker,
|
||||
placeholder: t(".ticker_placeholder"),
|
||||
required: true,
|
||||
class: "form-field__input border border-secondary rounded-lg px-3 py-2 w-full text-primary bg-container",
|
||||
pattern: "[A-Za-z0-9.:-]{1,20}",
|
||||
title: t(".ticker_hint") %>
|
||||
<p class="text-xs text-secondary"><%= t(".ticker_hint") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-1">
|
||||
<%= f.label :qty, t(".quantity_label"), class: "font-medium text-sm text-primary block" %>
|
||||
<%= f.number_field :qty,
|
||||
step: "any",
|
||||
placeholder: t(".quantity_placeholder"),
|
||||
class: "form-field__input border border-secondary rounded-lg px-3 py-2 w-full text-primary bg-container" %>
|
||||
<p class="text-xs text-secondary"><%= t(".quantity_hint") %></p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<%= f.label :price, t(".price_label"), class: "font-medium text-sm text-primary block" %>
|
||||
<%= f.number_field :price,
|
||||
step: "0.0001",
|
||||
min: "0",
|
||||
placeholder: t(".price_placeholder"),
|
||||
class: "form-field__input border border-secondary rounded-lg px-3 py-2 w-full text-primary bg-container" %>
|
||||
<p class="text-xs text-secondary"><%= t(".price_hint", currency: @entry.currency) %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-secondary bg-surface-inset p-2 rounded">
|
||||
<%= icon "info", size: "xs", class: "inline-block mr-1" %>
|
||||
<%= t(".qty_or_price_hint", amount: format_money(@entry.amount_money.abs)) %>
|
||||
</p>
|
||||
|
||||
<div class="space-y-1">
|
||||
<%= f.label :investment_activity_label, t(".trade_type_label"), class: "font-medium text-sm text-primary block" %>
|
||||
<%= f.select :investment_activity_label,
|
||||
options_for_select([[t("transactions.activity_labels.buy"), "Buy"], [t("transactions.activity_labels.sell"), "Sell"]], default_label),
|
||||
{},
|
||||
{ class: "form-field__input border border-secondary rounded-lg px-3 py-2 w-full text-primary bg-container" } %>
|
||||
<p class="text-xs text-secondary"><%= t(".trade_type_hint") %></p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<%= f.label :exchange_operating_mic, t(".exchange_label"), class: "font-medium text-sm text-primary block" %>
|
||||
<%= f.text_field :exchange_operating_mic,
|
||||
placeholder: t(".exchange_placeholder"),
|
||||
class: "form-field__input border border-secondary rounded-lg px-3 py-2 w-full text-primary bg-container",
|
||||
pattern: "[A-Z]{4}",
|
||||
title: t(".exchange_hint") %>
|
||||
<p class="text-xs text-secondary"><%= t(".exchange_hint") %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4 border-t border-primary">
|
||||
<%= render DS::Button.new(
|
||||
text: t(".cancel"),
|
||||
variant: "outline",
|
||||
href: "#",
|
||||
data: { action: "click->ds--dialog#close" }
|
||||
) %>
|
||||
<%= render DS::Button.new(
|
||||
text: t(".submit"),
|
||||
variant: "primary",
|
||||
type: "submit"
|
||||
) %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -265,6 +265,25 @@
|
||||
) %>
|
||||
</div>
|
||||
|
||||
<!-- Convert to Trade (Investment Accounts Only, not already converted) -->
|
||||
<% if @entry.account.investment? && @entry.entryable.is_a?(Transaction) && !@entry.excluded? %>
|
||||
<div class="flex items-center justify-between gap-2 p-3">
|
||||
<div class="text-sm space-y-1">
|
||||
<h4 class="text-primary">Convert to Security Trade</h4>
|
||||
<p class="text-secondary">Convert this transaction into a security trade (buy/sell) by providing ticker, shares, and price.</p>
|
||||
</div>
|
||||
|
||||
<%= render DS::Button.new(
|
||||
text: "Convert",
|
||||
variant: "outline",
|
||||
icon: "arrow-right-left",
|
||||
href: convert_to_trade_transaction_path(@entry.transaction),
|
||||
method: :get,
|
||||
frame: :modal
|
||||
) %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Mark as Recurring Form -->
|
||||
<div class="flex items-center justify-between gap-2 p-3">
|
||||
<div class="text-sm space-y-1">
|
||||
|
||||
Reference in New Issue
Block a user