mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-02 20:15:22 +02:00
Basic Portfolio Views (#1000)
* Add holdings tab to account view * Basic portfolio UI * Cleanup * Handle missing holding data * Remove synced at (implemented in separate pr) * translations * Tweak post sync streams * Remove stale methods from merge conflict
This commit is contained in:
parent
ef4be7948a
commit
7c2091b343
37 changed files with 582 additions and 86 deletions
|
@ -0,0 +1,15 @@
|
|||
<div class="fixed bottom-6 z-10 flex items-center justify-between rounded-xl bg-gray-900 px-4 text-sm text-white w-[420px] py-1.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= check_box_tag "entry_selection", 1, true, class: "maybe-checkbox maybe-checkbox--dark", data: { action: "bulk-select#deselectAll" } %>
|
||||
|
||||
<p data-bulk-select-target="selectionBarText"></p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1 text-gray-500">
|
||||
<%= form_with url: bulk_delete_transactions_path, data: { turbo_confirm: true, turbo_frame: "_top" } do %>
|
||||
<button type="button" data-bulk-select-scope-param="bulk_delete" data-action="bulk-select#submitBulkRequest" class="p-1.5 group hover:bg-gray-700 flex items-center justify-center rounded-md" title="Delete">
|
||||
<%= lucide_icon "trash-2", class: "w-5 group-hover:text-white" %>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
31
app/views/account/entries/entryables/trade/_show.html.erb
Normal file
31
app/views/account/entries/entryables/trade/_show.html.erb
Normal file
|
@ -0,0 +1,31 @@
|
|||
<%# locals: (entry:) %>
|
||||
|
||||
<% trade, account = entry.account_trade, entry.account %>
|
||||
|
||||
<%= drawer do %>
|
||||
<div>
|
||||
<header class="mb-4 space-y-1">
|
||||
<div class="flex items-center gap-4">
|
||||
<h3 class="font-medium">
|
||||
<span class="text-2xl"><%= format_money -entry.amount_money %></span>
|
||||
<span class="text-lg text-gray-500"><%= entry.currency %></span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<span class="text-sm text-gray-500"><%= entry.date.strftime("%A %d %B") %></span>
|
||||
</header>
|
||||
|
||||
<div class="space-y-2">
|
||||
<details class="group space-y-2" open>
|
||||
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
|
||||
<h4><%= t(".overview") %></h4>
|
||||
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
|
||||
</summary>
|
||||
|
||||
<div class="pb-6 pl-4 text-gray-500">
|
||||
<p>Details coming soon...</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
41
app/views/account/entries/entryables/trade/_trade.html.erb
Normal file
41
app/views/account/entries/entryables/trade/_trade.html.erb
Normal file
|
@ -0,0 +1,41 @@
|
|||
<%# locals: (entry:, selectable: true, **opts) %>
|
||||
|
||||
<% trade, account = entry.account_trade, entry.account %>
|
||||
|
||||
<div class="grid grid-cols-12 items-center text-gray-900 text-sm font-medium p-4">
|
||||
<div class="pr-10 flex items-center gap-4 col-span-6">
|
||||
<% if selectable %>
|
||||
<%= check_box_tag dom_id(entry, "selection"),
|
||||
class: "maybe-checkbox maybe-checkbox--light",
|
||||
data: { id: entry.id, "bulk-select-target": "row", action: "bulk-select#toggleRowSelection" } %>
|
||||
<% end %>
|
||||
|
||||
<div class="max-w-full">
|
||||
<%= tag.div class: ["flex items-center gap-2"] do %>
|
||||
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gray-600/5 text-gray-600">
|
||||
<%= entry.name[0].upcase %>
|
||||
</div>
|
||||
|
||||
<div class="truncate text-gray-900">
|
||||
<% if entry.new_record? %>
|
||||
<%= content_tag :p, entry.name %>
|
||||
<% else %>
|
||||
<%= link_to entry.name,
|
||||
account_entry_path(account, entry),
|
||||
data: { turbo_frame: "drawer", turbo_prefetch: false },
|
||||
class: "hover:underline hover:text-gray-800" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-1 col-span-3">
|
||||
<%= tag.p trade.buy? ? t(".buy") : t(".sell") %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-3 flex items-center justify-end">
|
||||
<%= tag.p format_money(entry.amount_money * -1), class: { "text-green-500": trade.sell? } %>
|
||||
</div>
|
||||
|
||||
</div>
|
42
app/views/account/entries/trades.html.erb
Normal file
42
app/views/account/entries/trades.html.erb
Normal file
|
@ -0,0 +1,42 @@
|
|||
<%= turbo_frame_tag dom_id(@account, "trades") do %>
|
||||
<div id="trades" data-controller="bulk-select" data-bulk-select-resource-value="<%= t(".trade") %>" class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="font-medium text-lg"><%= t(".trades") %></h3>
|
||||
<%= link_to new_account_entry_path(@account),
|
||||
disabled: true,
|
||||
class: "cursor-not-allowed flex gap-1 font-medium items-center bg-gray-50 text-gray-900 p-2 rounded-lg",
|
||||
data: { turbo_frame: :modal } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-900") %>
|
||||
<span class="text-sm"><%= t(".new") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-25 rounded-xl grid grid-cols-12 items-center uppercase text-xs font-medium text-gray-500 px-5 py-3">
|
||||
<div class="pl-0.5 col-span-6 flex items-center gap-4">
|
||||
<%= check_box_tag "selection_entry",
|
||||
class: "maybe-checkbox maybe-checkbox--light",
|
||||
data: { action: "bulk-select#togglePageSelection" } %>
|
||||
<%= tag.p t(".trade") %>
|
||||
</div>
|
||||
|
||||
<%= tag.p t(".type"), class: "col-span-3 justify-self-end" %>
|
||||
<%= tag.p t(".amount"), class: "col-span-3 justify-self-end" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div hidden id="transaction-selection-bar" data-bulk-select-target="selectionBar">
|
||||
<%= render "account/entries/entryables/trade/selection_bar" %>
|
||||
</div>
|
||||
|
||||
<% if @trades.empty? %>
|
||||
<p class="text-gray-500 py-4"><%= t(".no_trades") %></p>
|
||||
<% else %>
|
||||
<div class="space-y-6">
|
||||
<% @trades.group_by(&:date).each do |date, entries| %>
|
||||
<%= render "entry_group", date:, entries: entries %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
<div id="transactions" data-controller="bulk-select" data-bulk-select-resource-value="<%= t(".transaction") %>">
|
||||
<div hidden id="transaction-selection-bar" data-bulk-select-target="selectionBar">
|
||||
<%= render "selection_bar" %>
|
||||
<%= render "account/entries/entryables/transaction/selection_bar" %>
|
||||
</div>
|
||||
|
||||
<% if @transaction_entries.empty? %>
|
||||
|
|
45
app/views/account/holdings/_holding.html.erb
Normal file
45
app/views/account/holdings/_holding.html.erb
Normal file
|
@ -0,0 +1,45 @@
|
|||
<%# locals: (holding:) %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(holding) do %>
|
||||
<div class="grid grid-cols-12 items-center text-gray-900 text-sm font-medium p-4">
|
||||
<div class="col-span-4 flex items-center gap-4">
|
||||
<%= render "shared/circle_logo", name: holding.name %>
|
||||
<div>
|
||||
<%= link_to holding.name, account_holding_path(holding.account, holding), data: { turbo_frame: :drawer }, class: "hover:underline" %>
|
||||
<%= tag.p holding.symbol, class: "text-gray-500 text-xs uppercase" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 flex justify-end items-center gap-2">
|
||||
<% if holding.weight %>
|
||||
<%= render "shared/progress_circle", progress: holding.weight, text_class: "text-blue-500" %>
|
||||
<%= tag.p number_to_percentage(holding.weight, precision: 1) %>
|
||||
<% else %>
|
||||
<%= tag.p "?", class: "text-gray-500" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 text-right">
|
||||
<%= tag.p format_money holding.avg_cost %>
|
||||
<%= tag.p t(".per_share"), class: "font-normal text-gray-500" %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 text-right">
|
||||
<% if holding.amount_money %>
|
||||
<%= tag.p format_money holding.amount_money %>
|
||||
<% else %>
|
||||
<%= tag.p "?", class: "text-gray-500" %>
|
||||
<% end %>
|
||||
<%= tag.p t(".shares", qty: number_with_precision(holding.qty, precision: 1)), class: "font-normal text-gray-500" %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 text-right">
|
||||
<% if holding.trend %>
|
||||
<%= tag.p format_money(holding.trend.value), style: "color: #{holding.trend.color};" %>
|
||||
<%= tag.p "(#{number_to_percentage(holding.trend.percent, precision: 1)})", style: "color: #{holding.trend.color};" %>
|
||||
<% else %>
|
||||
<%= tag.p "?", class: "text-gray-500" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
1
app/views/account/holdings/_ruler.html.erb
Normal file
1
app/views/account/holdings/_ruler.html.erb
Normal file
|
@ -0,0 +1 @@
|
|||
<div class="h-px bg-alpha-black-50 ml-16 mr-4"></div>
|
37
app/views/account/holdings/index.html.erb
Normal file
37
app/views/account/holdings/index.html.erb
Normal file
|
@ -0,0 +1,37 @@
|
|||
<%= turbo_frame_tag dom_id(@account, "holdings") do %>
|
||||
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
|
||||
<div class="flex items-center justify-between">
|
||||
<%= tag.h2 t(".holdings"), class: "font-medium text-lg" %>
|
||||
<%= link_to new_account_holding_path(@account),
|
||||
disabled: true,
|
||||
data: { turbo_frame: :modal },
|
||||
class: "cursor-not-allowed flex gap-1 font-medium items-center bg-gray-50 text-gray-900 p-2 rounded-lg" do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-900") %>
|
||||
<%= tag.span t(".new_holding"), class: "text-sm" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl bg-gray-25 p-1">
|
||||
<div class="grid grid-cols-12 items-center uppercase text-xs font-medium text-gray-500 px-4 py-2">
|
||||
<%= tag.p t(".name"), class: "col-span-4" %>
|
||||
<%= tag.p t(".weight"), class: "col-span-2 justify-self-end" %>
|
||||
<%= tag.p t(".cost"), class: "col-span-2 justify-self-end" %>
|
||||
<%= tag.p t(".holdings"), class: "col-span-2 justify-self-end" %>
|
||||
<%= tag.p t(".return"), class: "col-span-2 justify-self-end" %>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg bg-white border-alpha-black-25 shadow-xs">
|
||||
<% if @holdings.any? %>
|
||||
<%= render partial: "account/holdings/holding", collection: @holdings, spacer_template: "ruler" %>
|
||||
<% elsif @account.needs_sync? || true %>
|
||||
<div class="flex flex-col justify-center items-center pt-4 pb-8">
|
||||
<p class="text-gray-500 p-4"><%= t(".needs_sync") %></p>
|
||||
<%= button_to "Sync holding prices", sync_account_path(@account), class: "bg-gray-900 text-white text-sm rounded-lg px-3 py-2" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-gray-500 text-sm p-4"><%= t(".no_holdings") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
1
app/views/account/holdings/new.html.erb
Normal file
1
app/views/account/holdings/new.html.erb
Normal file
|
@ -0,0 +1 @@
|
|||
<p>Coming soon...</p>
|
45
app/views/account/holdings/show.html.erb
Normal file
45
app/views/account/holdings/show.html.erb
Normal file
|
@ -0,0 +1,45 @@
|
|||
<%= drawer do %>
|
||||
<div class="space-y-4">
|
||||
<header class="flex justify-between">
|
||||
<div>
|
||||
<%= tag.h3 @holding.name, class: "text-2xl font-medium text-gray-900" %>
|
||||
<%= tag.p @holding.symbol.upcase, class: "text-sm text-gray-500" %>
|
||||
</div>
|
||||
|
||||
<%= render "shared/circle_logo", name: @holding.name %>
|
||||
</header>
|
||||
|
||||
<details class="group space-y-2">
|
||||
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
|
||||
<h4><%= t(".overview") %></h4>
|
||||
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
|
||||
</summary>
|
||||
|
||||
<div>
|
||||
<p class="pl-4 text-gray-500">Coming soon...</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group space-y-2">
|
||||
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
|
||||
<h4><%= t(".history") %></h4>
|
||||
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
|
||||
</summary>
|
||||
|
||||
<div>
|
||||
<p class="pl-4 text-gray-500">Coming soon...</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group space-y-2">
|
||||
<summary class="flex list-none items-center justify-between rounded-xl px-3 py-2 text-xs font-medium uppercase text-gray-500 bg-gray-25 focus-visible:outline-none">
|
||||
<h4><%= t(".settings") %></h4>
|
||||
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
|
||||
</summary>
|
||||
|
||||
<div>
|
||||
<p class="pl-4 text-gray-500">Coming soon...</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<% end %>
|
|
@ -1,6 +1,6 @@
|
|||
<%= turbo_stream_from @account %>
|
||||
|
||||
<div class="space-y-4">
|
||||
<%= tag.div id: dom_id(@account), class: "space-y-4" do %>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
<%= image_tag account_logo_url(@account), class: "w-8 h-8" %>
|
||||
|
@ -74,22 +74,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<% selected_tab = params[:tab] || "value" %>
|
||||
<% selected_tab_key, selected_tab_content_path = selected_account_tab(@account).values_at(:key, :content_path) %>
|
||||
|
||||
<div class="flex gap-2 text-sm text-gray-900 font-medium mb-4">
|
||||
<%= link_to t(".value"), account_path(tab: "value"), class: ["px-2 py-1.5 rounded-md border border-transparent", "bg-white shadow-xs border-alpha-black-50": selected_tab == "value"] %>
|
||||
<%= link_to t(".transactions"), account_path(tab: "transactions"), class: ["px-2 py-1.5 rounded-md border border-transparent", "bg-white shadow-xs border-alpha-black-50": selected_tab == "transactions"] %>
|
||||
<% account_tabs(@account).each do |tab| %>
|
||||
<%= link_to tab[:label], tab[:path], class: ["px-2 py-1.5 rounded-md border border-transparent", "bg-white shadow-xs border-alpha-black-50": selected_tab_key == tab[:key]] %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="min-h-[800px]">
|
||||
<% if selected_tab == "transactions" %>
|
||||
<%= turbo_frame_tag dom_id(@account, "transactions"), src: transaction_account_entries_path(@account) do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= turbo_frame_tag dom_id(@account, "valuations"), src: valuation_account_entries_path(@account) do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% end %>
|
||||
<%= turbo_frame_tag dom_id(@account, selected_tab_key), src: selected_tab_content_path do %>
|
||||
<%= render "account/entries/loading" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
</div>
|
||||
|
||||
<%= family_notifications_stream %>
|
||||
<%= family_stream %>
|
||||
|
||||
<%= content_for?(:content) ? yield(:content) : yield %>
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<%= turbo_frame_tag "drawer" do %>
|
||||
<dialog class="bg-white border border-alpha-black-25 rounded-2xl max-h-[calc(100vh-32px)] max-w-[480px] w-full shadow-xs h-full mt-4 mr-4 focus-visible:outline-none" data-controller="modal" data-action="click->modal#clickOutside">
|
||||
<div class="flex flex-col h-full p-4">
|
||||
<div class="flex justify-end items-center h-9">
|
||||
<div data-action="click->modal#close" class="cursor-pointer">
|
||||
<div class="flex justify-end items-center pb-4">
|
||||
<div data-action="click->modal#close" class="cursor-pointer p-2">
|
||||
<%= lucide_icon("x", class: "w-5 h-5 shrink-0") %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
<% if @transaction_entries.present? %>
|
||||
<div hidden id="entry-selection-bar" data-bulk-select-target="selectionBar">
|
||||
<%= render "account/entries/selection_bar" %>
|
||||
<%= render "account/entries/entryables/transaction/selection_bar" %>
|
||||
</div>
|
||||
<div class="grow overflow-y-auto">
|
||||
<div class="grid grid-cols-12 bg-gray-25 rounded-xl px-5 py-3 text-xs uppercase font-medium text-gray-500 items-center mb-4">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue