mirror of
https://github.com/we-promise/sure.git
synced 2026-04-11 00:04:47 +00:00
* Introduce SnapTrade integration with models, migrations, views, and activity processing logic. * Refactor SnapTrade activities processing: improve activity fetching flow, handle pending states, and update UI elements for enhanced user feedback. * Update Brakeman ignore file to include intentional redirect for SnapTrade OAuth portal. * Refactor SnapTrade models, views, and processing logic: add currency extraction helper, improve pending state handling, optimize migration checks, and enhance user feedback in UI. * Remove encryption for SnapTrade `snaptrade_user_id`, as it is an identifier, not a secret. * Introduce `SnaptradeConnectionCleanupJob` to asynchronously handle SnapTrade connection cleanup and improve i18n for SnapTrade item status messages. * Update SnapTrade encryption: make `snaptrade_user_secret` non-deterministic to enhance security. --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: luckyPipewrench <luckypipewrench@proton.me> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
110 lines
5.5 KiB
Plaintext
110 lines
5.5 KiB
Plaintext
<%# locals: (holding:, editable: true) %>
|
|
|
|
<%
|
|
# Pre-calculate values for the form
|
|
# Note: cost_basis field stores per-share cost, so calculate total for display
|
|
current_per_share = holding.cost_basis.present? && holding.cost_basis.positive? ? holding.cost_basis : nil
|
|
current_total = current_per_share && holding.qty.positive? ? (current_per_share * holding.qty).round(2) : nil
|
|
currency = Money::Currency.new(holding.currency)
|
|
%>
|
|
|
|
<%= turbo_frame_tag dom_id(holding, :cost_basis) do %>
|
|
<% if holding.cost_basis_locked? && !editable %>
|
|
<%# Locked and not editable (from holdings list) - just show value, right-aligned %>
|
|
<div class="flex items-center justify-end gap-1">
|
|
<%= tag.span format_money(holding.avg_cost) %>
|
|
<%= icon "lock", size: "xs", class: "text-secondary" %>
|
|
</div>
|
|
<% else %>
|
|
<%# Unlocked OR editable context (drawer) - show clickable menu %>
|
|
<%= render DS::Menu.new(variant: :button, placement: "bottom-end") do |menu| %>
|
|
<% menu.with_button(class: "hover:text-primary cursor-pointer group") do %>
|
|
<% if holding.avg_cost %>
|
|
<div class="flex items-center gap-1">
|
|
<%= tag.span format_money(holding.avg_cost) %>
|
|
<% if holding.cost_basis_locked? %>
|
|
<%= icon "lock", size: "xs", class: "text-secondary" %>
|
|
<% end %>
|
|
<%= icon "pencil", size: "xs", class: "text-secondary opacity-0 group-hover:opacity-100 transition-opacity" %>
|
|
</div>
|
|
<% else %>
|
|
<div class="flex items-center gap-1 px-2 py-0.5 rounded text-secondary hover:text-primary hover:bg-gray-100 theme-dark:hover:bg-gray-700 transition-colors">
|
|
<%= icon "pencil", size: "xs" %>
|
|
<span class="text-xs">Set</span>
|
|
</div>
|
|
<% end %>
|
|
<% end %>
|
|
<% menu.with_custom_content do %>
|
|
<div class="p-4 min-w-[280px]"
|
|
data-controller="cost-basis-form"
|
|
data-cost-basis-form-qty-value="<%= holding.qty %>">
|
|
<h4 class="font-medium text-sm mb-3">
|
|
<%= t(".set_cost_basis_header", ticker: holding.ticker, qty: format_quantity(holding.qty)) %>
|
|
</h4>
|
|
<%
|
|
form_data = { turbo: false }
|
|
if holding.avg_cost
|
|
form_data[:turbo_confirm] = {
|
|
title: t(".overwrite_confirm_title"),
|
|
body: t(".overwrite_confirm_body", current: format_money(holding.avg_cost))
|
|
}
|
|
end
|
|
%>
|
|
<%= styled_form_with model: holding,
|
|
url: holding_path(holding),
|
|
method: :patch,
|
|
class: "space-y-3",
|
|
data: form_data do |f| %>
|
|
<!-- Primary: Total cost basis (custom input, no spinners) -->
|
|
<div class="form-field">
|
|
<div class="form-field__body">
|
|
<label class="form-field__label"><%= t(".total_cost_basis_label") %></label>
|
|
<div class="flex items-center gap-1">
|
|
<span class="text-secondary text-sm font-medium"><%= currency.symbol %></span>
|
|
<input type="text" inputmode="decimal"
|
|
name="holding[cost_basis]"
|
|
class="form-field__input grow"
|
|
placeholder="0.00"
|
|
autocomplete="off"
|
|
value="<%= number_with_precision(current_total, precision: 2) if current_total %>"
|
|
data-action="input->cost-basis-form#updatePerShare"
|
|
data-cost-basis-form-target="total">
|
|
<span class="text-secondary text-sm"><%= currency.iso_code %></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="text-xs text-secondary -mt-2" data-cost-basis-form-target="perShareDisplay">
|
|
= <%= currency.symbol %><span data-cost-basis-form-target="perShareValue"><%= number_with_precision(current_per_share, precision: 2) || "0.00" %></span> <%= t(".per_share") %>
|
|
</p>
|
|
|
|
<!-- Alternative: Per-share input -->
|
|
<div class="pt-2 border-t border-tertiary">
|
|
<label class="text-xs text-secondary block mb-1"><%= t(".or_per_share_label") %></label>
|
|
<div class="flex items-center gap-1">
|
|
<span class="text-secondary text-sm font-medium"><%= currency.symbol %></span>
|
|
<input type="text" inputmode="decimal"
|
|
class="form-field__input grow"
|
|
placeholder="0.00"
|
|
autocomplete="off"
|
|
value="<%= number_with_precision(current_per_share, precision: 2) if current_per_share %>"
|
|
data-action="input->cost-basis-form#updateTotal"
|
|
data-cost-basis-form-target="perShare">
|
|
<span class="text-secondary text-sm"><%= currency.iso_code %></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-2 pt-2">
|
|
<button type="button"
|
|
class="inline-flex items-center gap-1 px-2 py-1 rounded-md text-sm font-medium text-primary bg-gray-200 hover:bg-gray-300 theme-dark:bg-gray-700 theme-dark:hover:bg-gray-600"
|
|
data-action="click->DS--menu#close">
|
|
<%= t(".cancel") %>
|
|
</button>
|
|
<%= f.submit t(".save"), class: "inline-flex items-center gap-1 px-2 py-1 rounded-md text-sm font-medium text-inverse bg-inverse hover:bg-inverse-hover" %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
<% end %>
|
|
<% end %>
|
|
<% end %>
|