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:
@@ -2,7 +2,7 @@ class SecuritiesController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
@securities = Security.search_provider(
|
@securities = Security.search_provider(
|
||||||
params[:q],
|
params[:q],
|
||||||
country_code: params[:country_code] == "US" ? "US" : nil
|
country_code: params[:country_code].presence
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -365,6 +365,8 @@ class TransactionsController < ApplicationController
|
|||||||
# Helper methods for convert_to_trade
|
# Helper methods for convert_to_trade
|
||||||
|
|
||||||
def resolve_security_for_conversion
|
def resolve_security_for_conversion
|
||||||
|
user_country = Current.family.country
|
||||||
|
|
||||||
if params[:security_id] == "__custom__"
|
if params[:security_id] == "__custom__"
|
||||||
# User selected "Enter custom ticker" - check for combobox selection or manual entry
|
# User selected "Enter custom ticker" - check for combobox selection or manual entry
|
||||||
if params[:ticker].present?
|
if params[:ticker].present?
|
||||||
@@ -372,13 +374,15 @@ class TransactionsController < ApplicationController
|
|||||||
ticker_symbol, exchange_operating_mic = params[:ticker].split("|")
|
ticker_symbol, exchange_operating_mic = params[:ticker].split("|")
|
||||||
Security::Resolver.new(
|
Security::Resolver.new(
|
||||||
ticker_symbol.strip,
|
ticker_symbol.strip,
|
||||||
exchange_operating_mic: exchange_operating_mic.presence || params[:exchange_operating_mic].presence
|
exchange_operating_mic: exchange_operating_mic.presence || params[:exchange_operating_mic].presence,
|
||||||
|
country_code: user_country
|
||||||
).resolve
|
).resolve
|
||||||
elsif params[:custom_ticker].present?
|
elsif params[:custom_ticker].present?
|
||||||
# Manual entry from combobox's name_when_new or fallback text field
|
# Manual entry from combobox's name_when_new or fallback text field
|
||||||
Security::Resolver.new(
|
Security::Resolver.new(
|
||||||
params[:custom_ticker].strip,
|
params[:custom_ticker].strip,
|
||||||
exchange_operating_mic: params[:exchange_operating_mic].presence
|
exchange_operating_mic: params[:exchange_operating_mic].presence,
|
||||||
|
country_code: user_country
|
||||||
).resolve
|
).resolve
|
||||||
else
|
else
|
||||||
flash[:alert] = t("transactions.convert_to_trade.errors.enter_ticker")
|
flash[:alert] = t("transactions.convert_to_trade.errors.enter_ticker")
|
||||||
@@ -398,13 +402,15 @@ class TransactionsController < ApplicationController
|
|||||||
ticker_symbol, exchange_operating_mic = params[:ticker].split("|")
|
ticker_symbol, exchange_operating_mic = params[:ticker].split("|")
|
||||||
Security::Resolver.new(
|
Security::Resolver.new(
|
||||||
ticker_symbol.strip,
|
ticker_symbol.strip,
|
||||||
exchange_operating_mic: exchange_operating_mic.presence || params[:exchange_operating_mic].presence
|
exchange_operating_mic: exchange_operating_mic.presence || params[:exchange_operating_mic].presence,
|
||||||
|
country_code: user_country
|
||||||
).resolve
|
).resolve
|
||||||
elsif params[:custom_ticker].present?
|
elsif params[:custom_ticker].present?
|
||||||
# Manual entry from combobox's name_when_new (no existing holdings path)
|
# Manual entry from combobox's name_when_new (no existing holdings path)
|
||||||
Security::Resolver.new(
|
Security::Resolver.new(
|
||||||
params[:custom_ticker].strip,
|
params[:custom_ticker].strip,
|
||||||
exchange_operating_mic: params[:exchange_operating_mic].presence
|
exchange_operating_mic: params[:exchange_operating_mic].presence,
|
||||||
|
country_code: user_country
|
||||||
).resolve
|
).resolve
|
||||||
end.tap do |security|
|
end.tap do |security|
|
||||||
if security.nil? && !performed?
|
if security.nil? && !performed?
|
||||||
|
|||||||
@@ -1,6 +1,132 @@
|
|||||||
class Security < ApplicationRecord
|
class Security < ApplicationRecord
|
||||||
include Provided
|
include Provided
|
||||||
|
|
||||||
|
# ISO 10383 MIC codes mapped to user-friendly exchange names
|
||||||
|
# Source: https://www.iso20022.org/market-identifier-codes
|
||||||
|
EXCHANGE_NAMES = {
|
||||||
|
# United States - NASDAQ family (Operating MIC: XNAS)
|
||||||
|
"XNAS" => "NASDAQ",
|
||||||
|
"XNGS" => "NASDAQ", # Global Select Market
|
||||||
|
"XNMS" => "NASDAQ", # Global Market
|
||||||
|
"XNCM" => "NASDAQ", # Capital Market
|
||||||
|
"XBOS" => "NASDAQ BX",
|
||||||
|
"XPSX" => "NASDAQ PSX",
|
||||||
|
"XNDQ" => "NASDAQ Options",
|
||||||
|
|
||||||
|
# United States - NYSE family (Operating MIC: XNYS)
|
||||||
|
"XNYS" => "NYSE",
|
||||||
|
"ARCX" => "NYSE Arca",
|
||||||
|
"XASE" => "NYSE American", # Formerly AMEX
|
||||||
|
"XCHI" => "NYSE Chicago",
|
||||||
|
"XCIS" => "NYSE National",
|
||||||
|
"AMXO" => "NYSE American Options",
|
||||||
|
"ARCO" => "NYSE Arca Options",
|
||||||
|
|
||||||
|
# United States - OTC Markets (Operating MIC: OTCM)
|
||||||
|
"OTCM" => "OTC Markets",
|
||||||
|
"PINX" => "OTC Pink",
|
||||||
|
"OTCQ" => "OTCQX",
|
||||||
|
"OTCB" => "OTCQB",
|
||||||
|
"PSGM" => "OTC Grey",
|
||||||
|
|
||||||
|
# United States - Other
|
||||||
|
"XCBO" => "CBOE",
|
||||||
|
"XCME" => "CME",
|
||||||
|
"XCBT" => "CBOT",
|
||||||
|
"XNYM" => "NYMEX",
|
||||||
|
"BATS" => "CBOE BZX",
|
||||||
|
"EDGX" => "CBOE EDGX",
|
||||||
|
"IEXG" => "IEX",
|
||||||
|
"MEMX" => "MEMX",
|
||||||
|
|
||||||
|
# United Kingdom
|
||||||
|
"XLON" => "London Stock Exchange",
|
||||||
|
"XLME" => "London Metal Exchange",
|
||||||
|
|
||||||
|
# Germany
|
||||||
|
"XETR" => "Xetra",
|
||||||
|
"XFRA" => "Frankfurt",
|
||||||
|
"XSTU" => "Stuttgart",
|
||||||
|
"XMUN" => "Munich",
|
||||||
|
"XBER" => "Berlin",
|
||||||
|
"XHAM" => "Hamburg",
|
||||||
|
"XDUS" => "Düsseldorf",
|
||||||
|
"XHAN" => "Hannover",
|
||||||
|
|
||||||
|
# Euronext
|
||||||
|
"XPAR" => "Euronext Paris",
|
||||||
|
"XAMS" => "Euronext Amsterdam",
|
||||||
|
"XBRU" => "Euronext Brussels",
|
||||||
|
"XLIS" => "Euronext Lisbon",
|
||||||
|
"XDUB" => "Euronext Dublin",
|
||||||
|
"XOSL" => "Euronext Oslo",
|
||||||
|
"XMIL" => "Euronext Milan",
|
||||||
|
|
||||||
|
# Other Europe
|
||||||
|
"XSWX" => "SIX Swiss",
|
||||||
|
"XVTX" => "SIX Swiss",
|
||||||
|
"XMAD" => "BME Madrid",
|
||||||
|
"XWBO" => "Vienna",
|
||||||
|
"XCSE" => "Copenhagen",
|
||||||
|
"XHEL" => "Helsinki",
|
||||||
|
"XSTO" => "Stockholm",
|
||||||
|
"XICE" => "Iceland",
|
||||||
|
"XPRA" => "Prague",
|
||||||
|
"XWAR" => "Warsaw",
|
||||||
|
"XATH" => "Athens",
|
||||||
|
"XIST" => "Istanbul",
|
||||||
|
|
||||||
|
# Canada
|
||||||
|
"XTSE" => "Toronto",
|
||||||
|
"XTSX" => "TSX Venture",
|
||||||
|
"XCNQ" => "CSE",
|
||||||
|
"NEOE" => "NEO",
|
||||||
|
|
||||||
|
# Australia & New Zealand
|
||||||
|
"XASX" => "ASX",
|
||||||
|
"XNZE" => "NZX",
|
||||||
|
|
||||||
|
# Asia - Japan
|
||||||
|
"XTKS" => "Tokyo",
|
||||||
|
"XJPX" => "Japan Exchange",
|
||||||
|
"XOSE" => "Osaka",
|
||||||
|
"XNGO" => "Nagoya",
|
||||||
|
"XSAP" => "Sapporo",
|
||||||
|
"XFKA" => "Fukuoka",
|
||||||
|
|
||||||
|
# Asia - China
|
||||||
|
"XSHG" => "Shanghai",
|
||||||
|
"XSHE" => "Shenzhen",
|
||||||
|
"XHKG" => "Hong Kong",
|
||||||
|
|
||||||
|
# Asia - Other
|
||||||
|
"XKRX" => "Korea Exchange",
|
||||||
|
"XKOS" => "KOSDAQ",
|
||||||
|
"XTAI" => "Taiwan",
|
||||||
|
"XSES" => "Singapore",
|
||||||
|
"XBKK" => "Thailand",
|
||||||
|
"XIDX" => "Indonesia",
|
||||||
|
"XKLS" => "Malaysia",
|
||||||
|
"XPHS" => "Philippines",
|
||||||
|
"XBOM" => "BSE India",
|
||||||
|
"XNSE" => "NSE India",
|
||||||
|
|
||||||
|
# Latin America
|
||||||
|
"XMEX" => "Mexico",
|
||||||
|
"XBUE" => "Buenos Aires",
|
||||||
|
"XBOG" => "Colombia",
|
||||||
|
"XSGO" => "Santiago",
|
||||||
|
"BVMF" => "B3 Brazil",
|
||||||
|
"XLIM" => "Lima",
|
||||||
|
|
||||||
|
# Middle East & Africa
|
||||||
|
"XTAE" => "Tel Aviv",
|
||||||
|
"XDFM" => "Dubai",
|
||||||
|
"XADS" => "Abu Dhabi",
|
||||||
|
"XSAU" => "Saudi (Tadawul)",
|
||||||
|
"XJSE" => "Johannesburg"
|
||||||
|
}.freeze
|
||||||
|
|
||||||
before_validation :upcase_symbols
|
before_validation :upcase_symbols
|
||||||
|
|
||||||
has_many :trades, dependent: :nullify, class_name: "Trade"
|
has_many :trades, dependent: :nullify, class_name: "Trade"
|
||||||
@@ -11,6 +137,16 @@ class Security < ApplicationRecord
|
|||||||
|
|
||||||
scope :online, -> { where(offline: false) }
|
scope :online, -> { where(offline: false) }
|
||||||
|
|
||||||
|
# Returns user-friendly exchange name for a MIC code
|
||||||
|
def self.exchange_name_for(mic)
|
||||||
|
return nil if mic.blank?
|
||||||
|
EXCHANGE_NAMES[mic.upcase] || mic.upcase
|
||||||
|
end
|
||||||
|
|
||||||
|
def exchange_name
|
||||||
|
self.class.exchange_name_for(exchange_operating_mic)
|
||||||
|
end
|
||||||
|
|
||||||
def current_price
|
def current_price
|
||||||
@current_price ||= find_or_fetch_price
|
@current_price ||= find_or_fetch_price
|
||||||
return nil if @current_price.nil?
|
return nil if @current_price.nil?
|
||||||
|
|||||||
@@ -7,7 +7,16 @@ class Security::ComboboxOption
|
|||||||
"#{symbol}|#{exchange_operating_mic}"
|
"#{symbol}|#{exchange_operating_mic}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def exchange_name
|
||||||
|
Security.exchange_name_for(exchange_operating_mic)
|
||||||
|
end
|
||||||
|
|
||||||
def to_combobox_display
|
def to_combobox_display
|
||||||
"#{symbol} - #{name} (#{exchange_operating_mic})"
|
I18n.t(
|
||||||
|
"securities.combobox.display",
|
||||||
|
symbol: symbol,
|
||||||
|
name: name,
|
||||||
|
exchange: exchange_name
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ module Security::Provided
|
|||||||
response = provider.search_securities(symbol, **params)
|
response = provider.search_securities(symbol, **params)
|
||||||
|
|
||||||
if response.success?
|
if response.success?
|
||||||
response.data.map do |provider_security|
|
securities = response.data.map do |provider_security|
|
||||||
# Need to map to domain model so Combobox can display via to_combobox_option
|
# Need to map to domain model so Combobox can display via to_combobox_option
|
||||||
Security.new(
|
Security.new(
|
||||||
ticker: provider_security.symbol,
|
ticker: provider_security.symbol,
|
||||||
@@ -31,6 +31,19 @@ module Security::Provided
|
|||||||
country_code: provider_security.country_code
|
country_code: provider_security.country_code
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Sort results to prioritize user's country if provided
|
||||||
|
if country_code.present?
|
||||||
|
user_country = country_code.upcase
|
||||||
|
securities.sort_by do |s|
|
||||||
|
[
|
||||||
|
s.country_code&.upcase == user_country ? 0 : 1, # User's country first
|
||||||
|
s.ticker.upcase == symbol.upcase ? 0 : 1 # Exact ticker match second
|
||||||
|
]
|
||||||
|
end
|
||||||
|
else
|
||||||
|
securities
|
||||||
|
end
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -124,9 +124,17 @@ class Security::Resolver
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Non-exhaustive list of common country codes for help in choosing "close" matches
|
# Non-exhaustive list of common country codes for help in choosing "close" matches
|
||||||
# These are generally sorted by market cap.
|
# User's country (if provided) is prioritized first, then sorted by market cap.
|
||||||
def sorted_country_codes_by_relevance
|
def sorted_country_codes_by_relevance
|
||||||
%w[US CN JP IN GB CA FR DE CH SA TW AU NL SE KR IE ES AE IT HK BR DK SG MX RU IL ID BE TH NO]
|
base_order = %w[US CN JP IN GB CA FR DE CH SA TW AU NL SE KR IE ES AE IT HK BR DK SG MX RU IL ID BE TH NO]
|
||||||
|
|
||||||
|
# Prioritize user's country if provided
|
||||||
|
if country_code.present?
|
||||||
|
user_country = country_code.upcase
|
||||||
|
[ user_country ] + (base_order - [ user_country ])
|
||||||
|
else
|
||||||
|
base_order
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Non-exhaustive list of common exchange operating MICs for help in choosing "close" matches
|
# Non-exhaustive list of common exchange operating MICs for help in choosing "close" matches
|
||||||
|
|||||||
@@ -8,13 +8,13 @@
|
|||||||
<%= combobox_security.name.presence || combobox_security.symbol %>
|
<%= combobox_security.name.presence || combobox_security.symbol %>
|
||||||
</span>
|
</span>
|
||||||
<span class="text-xs text-secondary">
|
<span class="text-xs text-secondary">
|
||||||
<%= "#{combobox_security.symbol} (#{combobox_security.exchange_operating_mic})" %>
|
<%= t("securities.combobox.exchange_label", symbol: combobox_security.symbol, exchange: combobox_security.exchange_name) %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<% if combobox_security.country_code.present? %>
|
<% if combobox_security.country_code.present? %>
|
||||||
<div class="flex items-center bg-container-inset rounded-sm px-1.5 py-1 gap-1">
|
<div class="flex items-center bg-container-inset rounded-sm px-1.5 py-1 gap-1">
|
||||||
<%= image_tag("https://hatscripts.github.io/circle-flags/flags/#{combobox_security.country_code.downcase}.svg",
|
<%= image_tag("https://hatscripts.github.io/circle-flags/flags/#{combobox_security.country_code.downcase}.svg",
|
||||||
class: "h-4 rounded-sm", # h-3 (12px) matches text-xs, w-5 for 3:5 aspect ratio
|
class: "h-4 rounded-sm",
|
||||||
alt: "#{combobox_security.country_code.upcase} flag",
|
alt: "#{combobox_security.country_code.upcase} flag",
|
||||||
title: combobox_security.country_code.upcase) %>
|
title: combobox_security.country_code.upcase) %>
|
||||||
<span class="text-xs text-secondary">
|
<span class="text-xs text-secondary">
|
||||||
|
|||||||
@@ -3,26 +3,6 @@
|
|||||||
# otherwise determine default based on amount sign
|
# otherwise determine default based on amount sign
|
||||||
# Negative amount (money going out) = Buy, Positive (money coming in) = Sell
|
# Negative amount (money going out) = Buy, Positive (money coming in) = Sell
|
||||||
default_label = params[:activity_label].presence || (@entry.amount > 0 ? "Sell" : "Buy")
|
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| %>
|
<%= 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-4">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<%= f.label :security_id, t(".security_label"), class: "font-medium text-sm text-primary block" %>
|
<%= f.label :security_id, t(".security_label"), class: "font-medium text-sm text-primary block" %>
|
||||||
<% if account_securities.any? %>
|
<% if Security.provider.present? %>
|
||||||
<%= f.select :security_id,
|
<%# Always use searchable combobox when provider available - prevents picking wrong similar tickers %>
|
||||||
options_for_select(
|
<div class="form-field combobox" style="--hw-handle-width: 0; --hw-handle-image: none;">
|
||||||
[[t(".security_prompt"), ""]] +
|
<%= f.combobox :ticker,
|
||||||
security_options_with_prices +
|
securities_path(country_code: Current.family.country),
|
||||||
[[t(".security_custom"), "__custom__"]],
|
name_when_new: "custom_ticker",
|
||||||
nil
|
placeholder: t(".ticker_search_placeholder"),
|
||||||
),
|
required: true %>
|
||||||
{},
|
</div>
|
||||||
{
|
<p class="text-xs text-secondary"><%= t(".ticker_search_hint") %></p>
|
||||||
class: "form-field__input border border-secondary rounded-lg px-3 py-2 w-full text-primary bg-container",
|
<% else %>
|
||||||
data: {
|
<%# No provider: show existing holdings dropdown with custom option %>
|
||||||
action: "change->convert-to-trade#toggleCustomTicker change->convert-to-trade#validatePrice",
|
<%
|
||||||
convert_to_trade_target: "tickerSelect"
|
# Get unique securities from account holdings for dropdown
|
||||||
}
|
account_securities = @entry.account.holdings
|
||||||
} %>
|
.includes(:security)
|
||||||
<p class="text-xs text-secondary mt-1"><%= t(".security_not_listed_hint") %></p>
|
.where.not(security: nil)
|
||||||
<div class="hidden mt-2" data-convert-to-trade-target="customWrapper">
|
.map(&:security)
|
||||||
<% if Security.provider.present? %>
|
.uniq(&:ticker)
|
||||||
<div class="form-field combobox">
|
.sort_by(&:ticker)
|
||||||
<%= f.combobox :ticker,
|
|
||||||
securities_path(country_code: Current.family.country),
|
# Build options with price data for validation
|
||||||
name_when_new: "custom_ticker",
|
security_options_with_prices = account_securities.map do |s|
|
||||||
placeholder: t(".ticker_search_placeholder"),
|
price = s.current_price
|
||||||
data: { convert_to_trade_target: "customField" } %>
|
price_display = price ? " (#{format_money(price)})" : ""
|
||||||
</div>
|
label = "#{s.ticker}#{price_display}"
|
||||||
<p class="text-xs text-secondary mt-1"><%= t(".ticker_search_hint") %></p>
|
label += " - #{s.name.truncate(25)}" if s.name.present?
|
||||||
<% else %>
|
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,
|
<%= f.text_field :custom_ticker,
|
||||||
placeholder: t(".ticker_placeholder"),
|
placeholder: t(".ticker_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",
|
||||||
@@ -88,18 +89,7 @@
|
|||||||
title: t(".ticker_hint"),
|
title: t(".ticker_hint"),
|
||||||
data: { convert_to_trade_target: "customField" } %>
|
data: { convert_to_trade_target: "customField" } %>
|
||||||
<p class="text-xs text-secondary mt-1"><%= t(".ticker_hint") %></p>
|
<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>
|
</div>
|
||||||
<p class="text-xs text-secondary"><%= t(".ticker_search_hint") %></p>
|
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= f.text_field :ticker,
|
<%= f.text_field :ticker,
|
||||||
placeholder: t(".ticker_placeholder"),
|
placeholder: t(".ticker_placeholder"),
|
||||||
@@ -166,15 +156,18 @@
|
|||||||
<p class="text-xs text-secondary"><%= t(".trade_type_hint") %></p>
|
<p class="text-xs text-secondary"><%= t(".trade_type_hint") %></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-1">
|
<%# Only show exchange field when no provider - combobox selections already include exchange %>
|
||||||
<%= f.label :exchange_operating_mic, t(".exchange_label"), class: "font-medium text-sm text-primary block" %>
|
<% unless Security.provider.present? %>
|
||||||
<%= f.text_field :exchange_operating_mic,
|
<div class="space-y-1">
|
||||||
placeholder: t(".exchange_placeholder"),
|
<%= f.label :exchange_operating_mic, t(".exchange_label"), class: "font-medium text-sm text-primary block" %>
|
||||||
class: "form-field__input border border-secondary rounded-lg px-3 py-2 w-full text-primary bg-container",
|
<%= f.text_field :exchange_operating_mic,
|
||||||
pattern: "[A-Z]{4}",
|
placeholder: t(".exchange_placeholder"),
|
||||||
title: t(".exchange_hint") %>
|
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(".exchange_hint") %></p>
|
pattern: "[A-Z]{4}",
|
||||||
</div>
|
title: t(".exchange_hint") %>
|
||||||
|
<p class="text-xs text-secondary"><%= t(".exchange_hint") %></p>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end gap-3 pt-4 border-t border-primary">
|
<div class="flex justify-end gap-3 pt-4 border-t border-primary">
|
||||||
|
|||||||
6
config/locales/views/securities/en.yml
Normal file
6
config/locales/views/securities/en.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
en:
|
||||||
|
securities:
|
||||||
|
combobox:
|
||||||
|
display: "%{symbol} - %{name} (%{exchange})"
|
||||||
|
exchange_label: "%{symbol} (%{exchange})"
|
||||||
Reference in New Issue
Block a user