mirror of
https://github.com/we-promise/sure.git
synced 2026-04-19 12:04:08 +00:00
Enhance ticker search and validation in "Convert to Trade" form (#688)
- Updated resolution logic to support combobox-based ticker selection and validation. - Added market price display with validation against entered prices to detect significant mismatches. - Improved messaging and UI for custom ticker input and market price warnings. Co-authored-by: luckyPipewrench <luckypipewrench@proton.me>
This commit is contained in:
@@ -5,12 +5,24 @@
|
||||
default_label = params[:activity_label].presence || (@entry.amount > 0 ? "Sell" : "Buy")
|
||||
|
||||
# Get unique securities from account holdings for ticker suggestions
|
||||
# Include current price for display and validation
|
||||
account_securities = @entry.account.holdings
|
||||
.includes(:security)
|
||||
.where.not(security: nil)
|
||||
.map(&:security)
|
||||
.uniq(&:ticker)
|
||||
.sort_by(&:ticker)
|
||||
|
||||
# Build options with price data for validation
|
||||
security_options_with_prices = account_securities.map do |s|
|
||||
price = s.current_price
|
||||
price_display = price ? " (#{format_money(price)})" : ""
|
||||
label = "#{s.ticker}#{price_display}"
|
||||
label += " - #{s.name.truncate(25)}" if s.name.present?
|
||||
# Store price as cents for JS comparison (amount is BigDecimal, multiply by 100 for cents)
|
||||
price_cents = price ? (price.amount * 100).to_i : nil
|
||||
[label, s.id, { "data-price-cents" => price_cents, "data-ticker" => s.ticker }]
|
||||
end
|
||||
%>
|
||||
|
||||
<%= render DS::Dialog.new(variant: "modal", reload_on_close: true) do |dialog| %>
|
||||
@@ -20,7 +32,7 @@
|
||||
<% 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| %>
|
||||
<%= form_with url: create_trade_from_transaction_transaction_path(@transaction), method: :post, class: "space-y-4", data: { controller: "convert-to-trade", turbo_frame: "_top", convert_to_trade_amount_cents_value: (@entry.amount_money.abs.amount * 100).to_i, convert_to_trade_currency_value: @entry.currency } 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">
|
||||
@@ -45,7 +57,7 @@
|
||||
<%= 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] } +
|
||||
security_options_with_prices +
|
||||
[[t(".security_custom"), "__custom__"]],
|
||||
nil
|
||||
),
|
||||
@@ -53,27 +65,50 @@
|
||||
{
|
||||
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",
|
||||
action: "change->convert-to-trade#toggleCustomTicker change->convert-to-trade#validatePrice",
|
||||
convert_to_trade_target: "tickerSelect"
|
||||
}
|
||||
} %>
|
||||
<p class="text-xs text-secondary mt-1"><%= t(".security_not_listed_hint") %></p>
|
||||
<div class="hidden mt-2" data-convert-to-trade-target="customWrapper">
|
||||
<%= f.text_field :custom_ticker,
|
||||
<% if Security.provider.present? %>
|
||||
<div class="form-field combobox">
|
||||
<%= f.combobox :ticker,
|
||||
securities_path(country_code: Current.family.country),
|
||||
name_when_new: "custom_ticker",
|
||||
placeholder: t(".ticker_search_placeholder"),
|
||||
data: { convert_to_trade_target: "customField" } %>
|
||||
</div>
|
||||
<p class="text-xs text-secondary mt-1"><%= t(".ticker_search_hint") %></p>
|
||||
<% else %>
|
||||
<%= 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" } %>
|
||||
<p class="text-xs text-secondary mt-1"><%= t(".ticker_hint") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<% if Security.provider.present? %>
|
||||
<div class="form-field combobox">
|
||||
<%= f.combobox :ticker,
|
||||
securities_path(country_code: Current.family.country),
|
||||
name_when_new: "custom_ticker",
|
||||
placeholder: t(".ticker_search_placeholder"),
|
||||
required: true %>
|
||||
</div>
|
||||
<p class="text-xs text-secondary"><%= t(".ticker_search_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"),
|
||||
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>
|
||||
title: t(".ticker_hint") %>
|
||||
<p class="text-xs text-secondary"><%= t(".ticker_hint") %></p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -83,7 +118,11 @@
|
||||
<%= 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" %>
|
||||
class: "form-field__input border border-secondary rounded-lg px-3 py-2 w-full text-primary bg-container",
|
||||
data: {
|
||||
action: "input->convert-to-trade#validatePrice",
|
||||
convert_to_trade_target: "qtyField"
|
||||
} %>
|
||||
<p class="text-xs text-secondary"><%= t(".quantity_hint") %></p>
|
||||
</div>
|
||||
|
||||
@@ -93,11 +132,26 @@
|
||||
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" %>
|
||||
class: "form-field__input border border-secondary rounded-lg px-3 py-2 w-full text-primary bg-container",
|
||||
data: {
|
||||
action: "input->convert-to-trade#validatePrice",
|
||||
convert_to_trade_target: "priceField"
|
||||
} %>
|
||||
<p class="text-xs text-secondary"><%= t(".price_hint", currency: @entry.currency) %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Price mismatch warning -->
|
||||
<div class="hidden p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg" data-convert-to-trade-target="priceWarning">
|
||||
<div class="flex items-start gap-2">
|
||||
<%= icon "alert-triangle", size: "sm", class: "text-yellow-600 dark:text-yellow-500 mt-0.5 shrink-0" %>
|
||||
<div class="text-sm">
|
||||
<p class="font-medium text-yellow-800 dark:text-yellow-200"><%= t(".price_mismatch_title") %></p>
|
||||
<p class="text-yellow-700 dark:text-yellow-300" data-convert-to-trade-target="priceWarningMessage"></p>
|
||||
</div>
|
||||
</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)) %>
|
||||
|
||||
Reference in New Issue
Block a user