mirror of
https://github.com/we-promise/sure.git
synced 2026-04-19 03:54:08 +00:00
Improve convert-to-trade security selection with search-first UX (#703)
* Enhance security handling logic: - Prioritize user's country in sorting securities and country codes. - Add comprehensive mapping for MIC codes to user-friendly exchange names. - Revamp combobox to consistently pull from a provider when available. - Improve handling of custom ticker and exchange input fields. * Localize securities combobox display and exchange labels. --------- Co-authored-by: luckyPipewrench <luckypipewrench@proton.me>
This commit is contained in:
@@ -3,26 +3,6 @@
|
||||
# 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
|
||||
# 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| %>
|
||||
@@ -53,34 +33,55 @@
|
||||
<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"), ""]] +
|
||||
security_options_with_prices +
|
||||
[[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 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">
|
||||
<% 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 %>
|
||||
<% if Security.provider.present? %>
|
||||
<%# Always use searchable combobox when provider available - prevents picking wrong similar tickers %>
|
||||
<div class="form-field combobox" style="--hw-handle-width: 0; --hw-handle-image: none;">
|
||||
<%= 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 %>
|
||||
<%# No provider: show existing holdings dropdown with custom option %>
|
||||
<%
|
||||
# Get unique securities from account holdings for dropdown
|
||||
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?
|
||||
price_cents = price ? (price.amount * 100).to_i : nil
|
||||
[label, s.id, { "data-price-cents" => price_cents, "data-ticker" => s.ticker }]
|
||||
end
|
||||
%>
|
||||
<% if account_securities.any? %>
|
||||
<%= f.select :security_id,
|
||||
options_for_select(
|
||||
[[t(".security_prompt"), ""]] +
|
||||
security_options_with_prices +
|
||||
[[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 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,
|
||||
placeholder: t(".ticker_placeholder"),
|
||||
class: "form-field__input border border-secondary rounded-lg px-3 py-2 w-full text-primary bg-container",
|
||||
@@ -88,18 +89,7 @@
|
||||
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"),
|
||||
@@ -166,15 +156,18 @@
|
||||
<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>
|
||||
<%# Only show exchange field when no provider - combobox selections already include exchange %>
|
||||
<% unless Security.provider.present? %>
|
||||
<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>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4 border-t border-primary">
|
||||
|
||||
Reference in New Issue
Block a user