mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-02 20:15:22 +02:00
Improve account sync performance, handle concurrent market data syncing (#2236)
* PlaidConnectable concern * Remove bad abstraction * Put sync implementations in own concerns * Sync strategies * Move sync orchestration to Sync class * Clean up sync class, add state machine * Basic market data sync cron * Fix price sync * Improve sync window column names, add timestamps * 30 day syncs by default * Clean up market data methods * Report high duplicate sync counts to Sentry * Add sync states throughout app * account tab session * Persistent account tab selections * Remove manual sleep * Add migration to clear stale syncs on self hosted apps * Tweak sync states * Sync completion event broadcasts * Fix timezones in tests * Cleanup * More cleanup * Plaid item UI broadcasts for sync * Fix account ID namespace conflict * Sync broadcasters * Smoother account sync refreshes * Remove test sync delay
This commit is contained in:
parent
9793cc74f9
commit
10dd9e061a
97 changed files with 1837 additions and 949 deletions
|
@ -30,9 +30,13 @@
|
|||
<% end %>
|
||||
</div>
|
||||
<div class="flex items-center gap-8">
|
||||
<p class="text-sm font-medium <%= account.is_active ? "text-primary" : "text-subdued" %>">
|
||||
<%= format_money account.balance_money %>
|
||||
</p>
|
||||
<% if account.syncing? %>
|
||||
<div class="w-16 h-6 bg-loader rounded-full animate-pulse"></div>
|
||||
<% else %>
|
||||
<p class="text-sm font-medium <%= account.is_active ? "text-primary" : "text-subdued" %>">
|
||||
<%= format_money account.balance_money %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<% unless account.scheduled_for_deletion? %>
|
||||
<%= styled_form_with model: account, data: { turbo_frame: "_top", controller: "auto-submit-form" } do |f| %>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<%# locals: (family:, active_account_group_tab:) %>
|
||||
<%# locals: (family:, active_tab:, mobile: false) %>
|
||||
|
||||
<div>
|
||||
<div id="account-sidebar-tabs">
|
||||
<% if family.missing_data_provider? %>
|
||||
<details class="group bg-yellow-tint-10 rounded-lg p-2 text-yellow-600 mb-3 text-xs">
|
||||
<summary class="flex items-center justify-between gap-2">
|
||||
|
@ -21,73 +21,71 @@
|
|||
</details>
|
||||
<% end %>
|
||||
|
||||
<div data-controller="sidebar-tabs">
|
||||
<%= render TabsComponent.new(active_tab: active_account_group_tab, url_param_key: "account_group_tab", testid: "account-sidebar-tabs") do |tabs| %>
|
||||
<% tabs.with_nav do |nav| %>
|
||||
<% nav.with_btn(id: "assets", label: "Assets") %>
|
||||
<% nav.with_btn(id: "debts", label: "Debts") %>
|
||||
<% nav.with_btn(id: "all", label: "All") %>
|
||||
<% end %>
|
||||
<%= render TabsComponent.new(active_tab: active_tab, session_key: "account_sidebar_tab", testid: "account-sidebar-tabs") do |tabs| %>
|
||||
<% tabs.with_nav do |nav| %>
|
||||
<% nav.with_btn(id: "asset", label: "Assets") %>
|
||||
<% nav.with_btn(id: "liability", label: "Debts") %>
|
||||
<% nav.with_btn(id: "all", label: "All") %>
|
||||
<% end %>
|
||||
|
||||
<% tabs.with_panel(tab_id: "assets") do %>
|
||||
<div class="space-y-2">
|
||||
<%= render LinkComponent.new(
|
||||
text: "New asset",
|
||||
variant: "ghost",
|
||||
href: new_account_path(step: "method_select", classification: "asset"),
|
||||
icon: "plus",
|
||||
frame: :modal,
|
||||
full_width: true,
|
||||
class: "justify-start"
|
||||
) %>
|
||||
|
||||
<div>
|
||||
<% family.balance_sheet.account_groups("asset").each do |group| %>
|
||||
<%= render "accounts/accountable_group", account_group: group %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% tabs.with_panel(tab_id: "debts") do %>
|
||||
<div class="space-y-2">
|
||||
<%= render LinkComponent.new(
|
||||
text: "New debt",
|
||||
<% tabs.with_panel(tab_id: "asset") do %>
|
||||
<div class="space-y-2">
|
||||
<%= render LinkComponent.new(
|
||||
text: "New asset",
|
||||
variant: "ghost",
|
||||
href: new_account_path(step: "method_select", classification: "liability"),
|
||||
href: new_account_path(step: "method_select", classification: "asset"),
|
||||
icon: "plus",
|
||||
frame: :modal,
|
||||
full_width: true,
|
||||
class: "justify-start"
|
||||
) %>
|
||||
|
||||
<div>
|
||||
<% family.balance_sheet.account_groups("liability").each do |group| %>
|
||||
<%= render "accounts/accountable_group", account_group: group %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% tabs.with_panel(tab_id: "all") do %>
|
||||
<div class="space-y-2">
|
||||
<%= render LinkComponent.new(
|
||||
text: "New account",
|
||||
variant: "ghost",
|
||||
full_width: true,
|
||||
href: new_account_path(step: "method_select"),
|
||||
icon: "plus",
|
||||
frame: :modal,
|
||||
class: "justify-start"
|
||||
) %>
|
||||
class: "justify-start"
|
||||
) %>
|
||||
|
||||
<div>
|
||||
<% family.balance_sheet.account_groups.each do |group| %>
|
||||
<%= render "accounts/accountable_group", account_group: group %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div>
|
||||
<% family.balance_sheet.account_groups("asset").each do |group| %>
|
||||
<%= render "accounts/accountable_group", account_group: group, mobile: mobile %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% tabs.with_panel(tab_id: "liability") do %>
|
||||
<div class="space-y-2">
|
||||
<%= render LinkComponent.new(
|
||||
text: "New debt",
|
||||
variant: "ghost",
|
||||
href: new_account_path(step: "method_select", classification: "liability"),
|
||||
icon: "plus",
|
||||
frame: :modal,
|
||||
full_width: true,
|
||||
class: "justify-start"
|
||||
) %>
|
||||
|
||||
<div>
|
||||
<% family.balance_sheet.account_groups("liability").each do |group| %>
|
||||
<%= render "accounts/accountable_group", account_group: group, mobile: mobile %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% tabs.with_panel(tab_id: "all") do %>
|
||||
<div class="space-y-2">
|
||||
<%= render LinkComponent.new(
|
||||
text: "New account",
|
||||
variant: "ghost",
|
||||
full_width: true,
|
||||
href: new_account_path(step: "method_select"),
|
||||
icon: "plus",
|
||||
frame: :modal,
|
||||
class: "justify-start"
|
||||
) %>
|
||||
|
||||
<div>
|
||||
<% family.balance_sheet.account_groups.each do |group| %>
|
||||
<%= render "accounts/accountable_group", account_group: group, mobile: mobile %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -1,49 +1,69 @@
|
|||
<%# locals: (account_group:) %>
|
||||
<%# locals: (account_group:, mobile: false, open: nil, **args) %>
|
||||
|
||||
<%= render DisclosureComponent.new(title: account_group.name, align: :left, open: account_group.accounts.any? { |account| page_active?(account_path(account)) }) do |disclosure| %>
|
||||
<% disclosure.with_summary_content do %>
|
||||
<div class="ml-auto text-right grow">
|
||||
<%= tag.p format_money(account_group.total_money), class: "text-sm font-medium text-primary" %>
|
||||
<div id="<%= mobile ? "mobile_#{account_group.id}" : account_group.id %>">
|
||||
<% is_open = open.nil? ? account_group.accounts.any? { |account| page_active?(account_path(account)) } : open %>
|
||||
<%= render DisclosureComponent.new(title: account_group.name, align: :left, open: is_open) do |disclosure| %>
|
||||
<% disclosure.with_summary_content do %>
|
||||
<div class="ml-auto text-right grow">
|
||||
<% if account_group.syncing? %>
|
||||
<div class="space-y-1">
|
||||
<div class="h-5 w-24 rounded ml-auto bg-loader"></div>
|
||||
<div class="flex items-center w-8 h-4 ml-auto">
|
||||
<div class="w-6 h-px bg-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= tag.p format_money(account_group.total_money), class: "text-sm font-medium text-primary" %>
|
||||
<%= turbo_frame_tag "#{account_group.key}_sparkline", src: accountable_sparkline_path(account_group.key), loading: "lazy" do %>
|
||||
<div class="flex items-center w-8 h-4 ml-auto">
|
||||
<div class="w-6 h-px bg-loader"></div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= turbo_frame_tag "#{account_group.key}_sparkline", src: accountable_sparkline_path(account_group.key), loading: "lazy" do %>
|
||||
<div class="flex items-center w-8 h-4 ml-auto">
|
||||
<div class="w-6 h-px bg-surface-inset"></div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-1">
|
||||
<% account_group.accounts.each do |account| %>
|
||||
<%= link_to account_path(account),
|
||||
<div class="space-y-1">
|
||||
<% account_group.accounts.each do |account| %>
|
||||
<%= link_to account_path(account),
|
||||
class: class_names(
|
||||
"block flex items-center gap-2 px-3 py-2 rounded-lg",
|
||||
page_active?(account_path(account)) ? "bg-container" : "hover:bg-surface-hover"
|
||||
),
|
||||
data: { sidebar_tabs_target: "account", action: "click->sidebar-tabs#select" },
|
||||
),
|
||||
title: account.name do %>
|
||||
<%= render "accounts/logo", account: account, size: "sm", color: account_group.color %>
|
||||
<%= render "accounts/logo", account: account, size: "sm", color: account_group.color %>
|
||||
|
||||
<div class="min-w-0 grow">
|
||||
<%= tag.p account.name, class: "text-sm text-primary font-medium mb-0.5 truncate" %>
|
||||
<%= tag.p account.short_subtype_label, class: "text-sm text-secondary truncate" %>
|
||||
</div>
|
||||
<div class="min-w-0 grow">
|
||||
<%= tag.p account.name, class: "text-sm text-primary font-medium mb-0.5 truncate" %>
|
||||
<%= tag.p account.short_subtype_label, class: "text-sm text-secondary truncate" %>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto text-right grow h-10">
|
||||
<%= tag.p format_money(account.balance_money), class: "text-sm font-medium text-primary whitespace-nowrap" %>
|
||||
<% if account.syncing? %>
|
||||
<div class="ml-auto text-right grow h-10">
|
||||
<div class="space-y-1">
|
||||
<div class="h-5 w-24 bg-loader rounded ml-auto"></div>
|
||||
<div class="flex items-center w-8 h-4 ml-auto">
|
||||
<div class="w-6 h-px bg-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="ml-auto text-right grow h-10">
|
||||
<%= tag.p format_money(account.balance_money), class: "text-sm font-medium text-primary whitespace-nowrap" %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(account, :sparkline), src: sparkline_account_path(account), loading: "lazy" do %>
|
||||
<div class="flex items-center w-8 h-5 ml-auto">
|
||||
<div class="w-6 h-px bg-surface-inset"></div>
|
||||
<%= turbo_frame_tag dom_id(account, :sparkline), src: sparkline_account_path(account), loading: "lazy" do %>
|
||||
<div class="flex items-center w-8 h-4 ml-auto">
|
||||
<div class="w-6 h-px bg-loader"></div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-2">
|
||||
<%= render LinkComponent.new(
|
||||
<div class="my-2">
|
||||
<%= render LinkComponent.new(
|
||||
href: new_polymorphic_path(account_group.key, step: "method_select"),
|
||||
text: "New #{account_group.name.downcase.singularize}",
|
||||
icon: "plus",
|
||||
|
@ -52,5 +72,6 @@
|
|||
frame: :modal,
|
||||
class: "justify-start"
|
||||
) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<div class="h-10">
|
||||
<div class="px-4">
|
||||
<div class="bg-loader rounded-md h-5 w-32"></div>
|
||||
</div>
|
||||
<div class="h-64 flex items-center justify-center">
|
||||
<p class="text-secondary animate-pulse text-sm">Loading...</p>
|
||||
|
||||
<div class="p-4 h-60 flex items-center justify-center">
|
||||
<div class="bg-loader rounded-md h-full w-full"></div>
|
||||
</div>
|
||||
|
|
|
@ -2,21 +2,25 @@
|
|||
<% trend = series.trend %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(@account, :chart_details) do %>
|
||||
<div class="px-4">
|
||||
<%= render partial: "shared/trend_change", locals: { trend: trend, comparison_label: @period.comparison_label } %>
|
||||
</div>
|
||||
<% if @account.syncing? %>
|
||||
<%= render "accounts/chart_loader" %>
|
||||
<% else %>
|
||||
<div class="px-4">
|
||||
<%= render partial: "shared/trend_change", locals: { trend: trend, comparison_label: @period.comparison_label } %>
|
||||
</div>
|
||||
|
||||
<div class="h-64 pb-4">
|
||||
<% if series.any? %>
|
||||
<div
|
||||
<div class="h-64 pb-4">
|
||||
<% if series.any? %>
|
||||
<div
|
||||
id="lineChart"
|
||||
class="w-full h-full"
|
||||
data-controller="time-series-chart"
|
||||
data-time-series-chart-data-value="<%= series.to_json %>"></div>
|
||||
<% else %>
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<p class="text-secondary text-sm"><%= t(".data_not_available") %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<p class="text-secondary text-sm"><%= t(".data_not_available") %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
|
|
@ -2,17 +2,6 @@
|
|||
<h1 class="text-xl"><%= t(".accounts") %></h1>
|
||||
<div class="flex items-center gap-5">
|
||||
<div class="flex items-center gap-2">
|
||||
<% if Rails.env.development? %>
|
||||
<%= render ButtonComponent.new(
|
||||
text: "Sync all",
|
||||
href: sync_all_accounts_path,
|
||||
method: :post,
|
||||
variant: "outline",
|
||||
disabled: Current.family.syncing?,
|
||||
icon: "refresh-cw",
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render LinkComponent.new(
|
||||
text: "New account",
|
||||
href: new_account_path(return_to: accounts_path),
|
||||
|
|
|
@ -6,9 +6,12 @@
|
|||
<p><%= Accountable.from_type(group).display_name %></p>
|
||||
<span class="text-subdued mx-2">·</span>
|
||||
<p><%= accounts.count %></p>
|
||||
<p class="ml-auto"><%= totals_by_currency(collection: accounts, money_method: :balance_money) %></p>
|
||||
|
||||
<% unless accounts.any?(&:syncing?) %>
|
||||
<p class="ml-auto"><%= totals_by_currency(collection: accounts, money_method: :balance_money) %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="bg-container">
|
||||
<div class="bg-container rounded-md">
|
||||
<% accounts.each do |account| %>
|
||||
<%= render account %>
|
||||
<% end %>
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
<%# locals: (account:, title: nil, tooltip: nil, chart_view: nil, **args) %>
|
||||
<%# locals: (account:, tooltip: nil, chart_view: nil, **args) %>
|
||||
|
||||
<% period = @period || Period.last_30_days %>
|
||||
<% default_value_title = account.asset? ? t(".balance") : t(".owed") %>
|
||||
|
||||
<div id="<%= dom_id(account, :chart) %>" class="bg-container shadow-xs rounded-xl border border-alpha-black-25 rounded-lg space-y-2">
|
||||
<div id="<%= dom_id(account, :chart) %>" class="bg-container shadow-border-xs rounded-xl space-y-2">
|
||||
<div class="flex justify-between flex-col-reverse lg:flex-row gap-2 px-4 pt-4 mb-2">
|
||||
<div class="space-y-2 w-full">
|
||||
<div class="flex items-center gap-1">
|
||||
<%= tag.p title || default_value_title, class: "text-sm font-medium text-secondary" %>
|
||||
<%= tooltip %>
|
||||
<%= tag.p account.investment? ? "Total value" : default_value_title, class: "text-sm font-medium text-secondary" %>
|
||||
|
||||
<% if !account.syncing? && account.investment? %>
|
||||
<%= render "investments/value_tooltip", balance: account.balance_money, holdings: account.balance_money - account.cash_balance_money, cash: account.cash_balance_money %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= tag.p format_money(account.balance_money), class: "text-primary text-3xl font-medium truncate" %>
|
||||
<% if account.syncing? %>
|
||||
<div class="bg-loader rounded-md h-7 w-20"></div>
|
||||
<% else %>
|
||||
<%= tag.p format_money(account.balance_money), class: "text-primary text-3xl font-medium truncate" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= form_with url: request.path, method: :get, data: { controller: "auto-submit-form" } do |form| %>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<%# locals: (account:, header: nil, chart: nil, tabs: nil) %>
|
||||
<%# locals: (account:, header: nil, chart: nil, chart_view: nil, tabs: nil) %>
|
||||
|
||||
<%= turbo_stream_from account %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(account) do %>
|
||||
<%= turbo_frame_tag dom_id(account, :container) do %>
|
||||
<%= tag.div class: "space-y-4 pb-32" do %>
|
||||
<% if header.present? %>
|
||||
<%= header %>
|
||||
|
@ -13,7 +13,7 @@
|
|||
<% if chart.present? %>
|
||||
<%= chart %>
|
||||
<% else %>
|
||||
<%= render "accounts/show/chart", account: account %>
|
||||
<%= render "accounts/show/chart", account: account, chart_view: chart_view %>
|
||||
<% end %>
|
||||
|
||||
<div class="min-h-[800px]" data-testid="account-details">
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
<div class="flex flex-col relative" data-controller="list-filter">
|
||||
<div class="grow p-1.5">
|
||||
<div class="relative flex items-center bg-container border border-secondary rounded-lg">
|
||||
<input
|
||||
placeholder="<%= t(".search_placeholder") %>"
|
||||
autocomplete="nope"
|
||||
type="search"
|
||||
class="bg-container placeholder:text-sm placeholder:text-secondary font-normal h-10 relative pl-10 w-full border-none rounded-lg focus:outline-hidden focus:ring-0"
|
||||
data-list-filter-target="input"
|
||||
data-action="list-filter#filter" />
|
||||
<input
|
||||
placeholder="<%= t(".search_placeholder") %>"
|
||||
autocomplete="nope"
|
||||
type="search"
|
||||
class="bg-container placeholder:text-sm placeholder:text-secondary font-normal h-10 relative pl-10 w-full border-none rounded-lg focus:outline-hidden focus:ring-0"
|
||||
data-list-filter-target="input"
|
||||
data-action="list-filter#filter">
|
||||
<%= icon("search", class: "absolute inset-0 ml-2 transform top-1/2 -translate-y-1/2") %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -19,8 +19,13 @@
|
|||
|
||||
<div class="col-span-2 flex justify-end items-center gap-2">
|
||||
<% cash_weight = account.balance.zero? ? 0 : account.cash_balance / account.balance * 100 %>
|
||||
<%= render "shared/progress_circle", progress: cash_weight %>
|
||||
<%= tag.p number_to_percentage(cash_weight, precision: 1) %>
|
||||
|
||||
<% if account.syncing? %>
|
||||
<div class="w-16 h-6 bg-loader rounded-full"></div>
|
||||
<% else %>
|
||||
<%= render "shared/progress_circle", progress: cash_weight %>
|
||||
<%= tag.p number_to_percentage(cash_weight, precision: 1) %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 text-right">
|
||||
|
@ -28,7 +33,13 @@
|
|||
</div>
|
||||
|
||||
<div class="col-span-2 text-right">
|
||||
<%= tag.p format_money account.cash_balance_money %>
|
||||
<% if account.syncing? %>
|
||||
<div class="flex justify-end">
|
||||
<div class="w-16 h-6 bg-loader rounded-full"></div>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= tag.p format_money account.cash_balance_money %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 text-right">
|
||||
|
|
|
@ -17,7 +17,9 @@
|
|||
</div>
|
||||
|
||||
<div class="col-span-2 flex justify-end items-center gap-2">
|
||||
<% if holding.weight %>
|
||||
<% if holding.account.syncing? %>
|
||||
<div class="w-16 h-6 bg-loader rounded-full"></div>
|
||||
<% elsif holding.weight %>
|
||||
<%= render "shared/progress_circle", progress: holding.weight %>
|
||||
<%= tag.p number_to_percentage(holding.weight, precision: 1) %>
|
||||
<% else %>
|
||||
|
@ -26,21 +28,39 @@
|
|||
</div>
|
||||
|
||||
<div class="col-span-2 text-right">
|
||||
<%= tag.p format_money holding.avg_cost %>
|
||||
<%= tag.p t(".per_share"), class: "font-normal text-secondary" %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 text-right">
|
||||
<% if holding.amount_money %>
|
||||
<%= tag.p format_money holding.amount_money %>
|
||||
<% if holding.account.syncing? %>
|
||||
<div class="flex justify-end">
|
||||
<div class="w-16 h-6 bg-loader rounded-full"></div>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= tag.p "--", class: "text-secondary" %>
|
||||
<%= tag.p format_money holding.avg_cost %>
|
||||
<%= tag.p t(".per_share"), class: "font-normal text-secondary" %>
|
||||
<% end %>
|
||||
<%= tag.p t(".shares", qty: number_with_precision(holding.qty, precision: 1)), class: "font-normal text-secondary" %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 text-right">
|
||||
<% if holding.trend %>
|
||||
<% if holding.account.syncing? %>
|
||||
<div class="flex flex-col gap-2 items-end">
|
||||
<div class="w-16 h-4 bg-loader rounded-full"></div>
|
||||
<div class="w-16 h-2 bg-loader rounded-full"></div>
|
||||
</div>
|
||||
<% else %>
|
||||
<% if holding.amount_money %>
|
||||
<%= tag.p format_money holding.amount_money %>
|
||||
<% else %>
|
||||
<%= tag.p "--", class: "text-secondary" %>
|
||||
<% end %>
|
||||
<%= tag.p t(".shares", qty: number_with_precision(holding.qty, precision: 1)), class: "font-normal text-secondary" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 text-right">
|
||||
<% if holding.account.syncing? %>
|
||||
<div class="flex flex-col gap-2 items-end">
|
||||
<div class="w-16 h-4 bg-loader rounded-full"></div>
|
||||
<div class="w-16 h-2 bg-loader rounded-full"></div>
|
||||
</div>
|
||||
<% elsif 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 %>
|
||||
|
|
|
@ -1,25 +1,7 @@
|
|||
<%= turbo_stream_from @account %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(@account) do %>
|
||||
<%= tag.div class: "space-y-4" do %>
|
||||
<%= render "accounts/show/header", account: @account %>
|
||||
|
||||
<%= render "accounts/show/chart",
|
||||
account: @account,
|
||||
title: t(".chart_title"),
|
||||
chart_view: @chart_view,
|
||||
tooltip: render(
|
||||
"investments/value_tooltip",
|
||||
balance: @account.balance_money,
|
||||
holdings: @account.balance_money - @account.cash_balance_money,
|
||||
cash: @account.cash_balance_money
|
||||
) %>
|
||||
|
||||
<div class="min-h-[800px]">
|
||||
<%= render "accounts/show/tabs", account: @account, tabs: [
|
||||
{ key: "activity", contents: render("accounts/show/activity", account: @account) },
|
||||
{ key: "holdings", contents: render("investments/holdings_tab", account: @account) },
|
||||
] %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= render "accounts/show/template",
|
||||
account: @account,
|
||||
chart_view: @chart_view,
|
||||
tabs: render("accounts/show/tabs", account: @account, tabs: [
|
||||
{ key: "activity", contents: render("accounts/show/activity", account: @account) },
|
||||
{ key: "holdings", contents: render("investments/holdings_tab", account: @account) },
|
||||
]) %>
|
||||
|
|
|
@ -26,7 +26,8 @@
|
|||
<%= render(
|
||||
"accounts/account_sidebar_tabs",
|
||||
family: Current.family,
|
||||
active_account_group_tab: params[:account_group_tab] || "assets"
|
||||
active_tab: @account_group_tab,
|
||||
mobile: true
|
||||
) %>
|
||||
</div>
|
||||
|
||||
|
@ -80,8 +81,8 @@
|
|||
<%= yield :sidebar %>
|
||||
<% else %>
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="overflow-y-auto grow" id="account-sidebar-tabs" data-turbo-permanent>
|
||||
<%= render "accounts/account_sidebar_tabs", family: Current.family, active_account_group_tab: params[:account_group_tab] || "assets" %>
|
||||
<div class="overflow-y-auto grow">
|
||||
<%= render "accounts/account_sidebar_tabs", family: Current.family, active_tab: @account_group_tab %>
|
||||
</div>
|
||||
|
||||
<% if Current.family.trialing? && !self_hosted? %>
|
||||
|
|
|
@ -23,10 +23,6 @@
|
|||
<%= render_flash_notifications %>
|
||||
|
||||
<div id="cta"></div>
|
||||
|
||||
<% if Current.family&.syncing? %>
|
||||
<%= render "shared/notifications/loading", id: "syncing-notice", message: "Syncing accounts data..." %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -25,11 +25,14 @@
|
|||
|
||||
<div class="w-full space-y-6 pb-24">
|
||||
<% if Current.family.accounts.any? %>
|
||||
<section class="bg-container py-4 rounded-xl shadow-border-xs px-0.5">
|
||||
<%= render partial: "pages/dashboard/net_worth_chart", locals: { series: @balance_sheet.net_worth_series(period: @period), period: @period } %>
|
||||
<section class="bg-container py-4 rounded-xl shadow-border-xs">
|
||||
<%= render partial: "pages/dashboard/net_worth_chart", locals: {
|
||||
balance_sheet: @balance_sheet,
|
||||
period: @period
|
||||
} %>
|
||||
</section>
|
||||
<% else %>
|
||||
<section class="p-0.5">
|
||||
<section>
|
||||
<%= render "pages/dashboard/no_accounts_graph_placeholder" %>
|
||||
</section>
|
||||
<% end %>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<%# locals: (balance_sheet:) %>
|
||||
<%# locals: (balance_sheet:, **args) %>
|
||||
|
||||
<div class="space-y-4 overflow-x-auto p-0.5">
|
||||
<div class="space-y-4" id="balance-sheet">
|
||||
<% balance_sheet.classification_groups.each do |classification_group| %>
|
||||
<div class="bg-container shadow-border-xs rounded-xl space-y-4 p-4">
|
||||
<h2 class="text-lg font-medium inline-flex items-center gap-1.5">
|
||||
|
@ -11,26 +11,38 @@
|
|||
<% if classification_group.account_groups.any? %>
|
||||
<span class="text-secondary">·</span>
|
||||
|
||||
<span class="text-secondary font-medium text-lg"><%= classification_group.total_money.format(precision: 0) %></span>
|
||||
<% if classification_group.syncing? %>
|
||||
<div class="flex items-center w-8 h-4 ml-auto">
|
||||
<div class="bg-loader w-full h-full rounded-md"></div>
|
||||
</div>
|
||||
<% else %>
|
||||
<span class="text-secondary font-medium text-lg"><%= classification_group.total_money.format(precision: 0) %></span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</h2>
|
||||
|
||||
<% if classification_group.account_groups.any? %>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex gap-1">
|
||||
<% classification_group.account_groups.each do |account_group| %>
|
||||
<div class="h-1.5 rounded-sm" style="width: <%= account_group.weight %>%; background-color: <%= account_group.color %>;"></div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<% classification_group.account_groups.each do |account_group| %>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<div class="h-2.5 w-2.5 rounded-full" style="background-color: <%= account_group.color %>;"></div>
|
||||
<p class="text-secondary"><%= account_group.name %></p>
|
||||
<p class="text-primary font-mono"><%= number_to_percentage(account_group.weight, precision: 0) %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if classification_group.syncing? %>
|
||||
<p class="text-xs text-subdued animate-pulse">Calculating latest balance data...</p>
|
||||
<% else %>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<% classification_group.account_groups.each do |account_group| %>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<div class="h-2.5 w-2.5 rounded-full" style="background-color: <%= account_group.color %>;"></div>
|
||||
<p class="text-secondary"><%= account_group.name %></p>
|
||||
<p class="text-primary font-mono"><%= number_to_percentage(account_group.weight, precision: 0) %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="bg-surface rounded-xl p-1 space-y-1 overflow-x-auto">
|
||||
|
@ -56,15 +68,27 @@
|
|||
<p><%= account_group.name %></p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between text-right gap-6">
|
||||
<div class="w-28 shrink-0 flex items-center justify-end gap-2">
|
||||
<%= render "pages/dashboard/group_weight", weight: account_group.weight, color: account_group.color %>
|
||||
</div>
|
||||
<% if account_group.syncing? %>
|
||||
<div class="flex items-center justify-between text-right gap-6">
|
||||
<div class="w-28 shrink-0 flex items-center justify-end gap-2">
|
||||
<div class="bg-loader rounded-md h-4 w-12"></div>
|
||||
</div>
|
||||
|
||||
<div class="w-40 shrink-0">
|
||||
<p><%= format_money(account_group.total_money) %></p>
|
||||
<div class="w-40 shrink-0 flex justify-end">
|
||||
<div class="bg-loader rounded-md h-4 w-12"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-between text-right gap-6">
|
||||
<div class="w-28 shrink-0 flex items-center justify-end gap-2">
|
||||
<%= render "pages/dashboard/group_weight", weight: account_group.weight, color: account_group.color %>
|
||||
</div>
|
||||
|
||||
<div class="w-40 shrink-0">
|
||||
<p><%= format_money(account_group.total_money) %></p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</summary>
|
||||
|
||||
<div>
|
||||
|
@ -76,15 +100,27 @@
|
|||
<%= link_to account.name, account_path(account) %>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center text-right gap-6">
|
||||
<div class="w-28 shrink-0 flex items-center justify-end gap-2">
|
||||
<%= render "pages/dashboard/group_weight", weight: account.weight, color: account_group.color %>
|
||||
</div>
|
||||
<% if account.syncing? %>
|
||||
<div class="ml-auto flex items-center text-right gap-6">
|
||||
<div class="w-28 shrink-0 flex items-center justify-end gap-2">
|
||||
<div class="bg-loader rounded-md h-4 w-12"></div>
|
||||
</div>
|
||||
|
||||
<div class="w-40 shrink-0">
|
||||
<p><%= format_money(account.balance_money) %></p>
|
||||
<div class="w-40 shrink-0 flex justify-end">
|
||||
<div class="bg-loader rounded-md h-4 w-12"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="ml-auto flex items-center text-right gap-6">
|
||||
<div class="w-28 shrink-0 flex items-center justify-end gap-2">
|
||||
<%= render "pages/dashboard/group_weight", weight: account.weight, color: account_group.color %>
|
||||
</div>
|
||||
|
||||
<div class="w-40 shrink-0">
|
||||
<p><%= format_money(account.balance_money) %></p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if idx < account_group.accounts.size - 1 %>
|
||||
|
|
|
@ -1,37 +1,55 @@
|
|||
<%# locals: (series:, period:) %>
|
||||
<%# locals: (balance_sheet:, period:, **args) %>
|
||||
|
||||
<div class="flex justify-between gap-4 px-4">
|
||||
<div class="space-y-2">
|
||||
<div id="net-worth-chart">
|
||||
<% series = balance_sheet.net_worth_series(period: period) %>
|
||||
<div class="flex justify-between gap-4 px-4">
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm text-secondary font-medium"><%= t(".title") %></p>
|
||||
<p class="text-primary -space-x-0.5 text-3xl font-medium">
|
||||
<%= series.current.format %>
|
||||
</p>
|
||||
<% if series.trend.nil? %>
|
||||
<p class="text-sm text-secondary"><%= t(".data_not_available") %></p>
|
||||
<% else %>
|
||||
<%= render partial: "shared/trend_change", locals: { trend: series.trend, comparison_label: period.comparison_label } %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm text-secondary font-medium"><%= t(".title") %></p>
|
||||
|
||||
<%= form_with url: root_path, method: :get, data: { controller: "auto-submit-form" } do |form| %>
|
||||
<%= form.select :period,
|
||||
<% if balance_sheet.syncing? %>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="bg-loader rounded-md h-7 w-20"></div>
|
||||
<div class="bg-loader rounded-md h-5 w-32"></div>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-primary -space-x-0.5 text-3xl font-medium">
|
||||
<%= series.current.format %>
|
||||
</p>
|
||||
|
||||
<% if series.trend.nil? %>
|
||||
<p class="text-sm text-secondary"><%= t(".data_not_available") %></p>
|
||||
<% else %>
|
||||
<%= render partial: "shared/trend_change", locals: { trend: series.trend, comparison_label: period.comparison_label } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= form_with url: root_path, method: :get, data: { controller: "auto-submit-form" } do |form| %>
|
||||
<%= form.select :period,
|
||||
Period.as_options,
|
||||
{ selected: period.key },
|
||||
data: { "auto-submit-form-target": "auto" },
|
||||
class: "bg-container border border-secondary font-medium rounded-lg px-3 py-2 text-sm pr-7 cursor-pointer text-primary focus:outline-hidden focus:ring-0" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if series.any? %>
|
||||
<div
|
||||
<% if balance_sheet.syncing? %>
|
||||
<div class="w-full flex items-center justify-center p-4 h-52">
|
||||
<div class="bg-loader rounded-md h-full w-full"></div>
|
||||
</div>
|
||||
<% else %>
|
||||
<% if series.any? %>
|
||||
<div
|
||||
id="netWorthChart"
|
||||
class="w-full flex-1 min-h-52"
|
||||
data-controller="time-series-chart"
|
||||
data-time-series-chart-data-value="<%= series.to_json %>"></div>
|
||||
<% else %>
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<p class="text-secondary text-sm"><%= t(".data_not_available") %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<p class="text-secondary text-sm"><%= t(".data_not_available") %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<%# locals: (rule:) %>
|
||||
<div class="flex justify-between items-center p-4 <%= rule.active? ? 'text-primary' : 'text-secondary' %>">
|
||||
<div class="flex justify-between items-center p-4 <%= rule.active? ? "text-primary" : "text-secondary" %>">
|
||||
<div class="text-sm space-y-1.5">
|
||||
<% if rule.name.present? %>
|
||||
<h3 class="font-medium text-md"><%= rule.name %></h3>
|
||||
|
@ -49,7 +49,7 @@
|
|||
<% if rule.effective_date.nil? %>
|
||||
All past and future <%= rule.resource_type.pluralize %>
|
||||
<% else %>
|
||||
<%= rule.resource_type.pluralize %> on or after <%= rule.effective_date.strftime('%b %-d, %Y') %>
|
||||
<%= rule.resource_type.pluralize %> on or after <%= rule.effective_date.strftime("%b %-d, %Y") %>
|
||||
<% end %>
|
||||
</span>
|
||||
</p>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<%= render DialogComponent.new(reload_on_close: true) do |dialog| %>
|
||||
<%
|
||||
<%
|
||||
title = if @rule.name.present?
|
||||
"Confirm changes to \"#{@rule.name}\""
|
||||
else
|
||||
|
@ -7,7 +7,7 @@
|
|||
end
|
||||
%>
|
||||
<% dialog.with_header(title: title) %>
|
||||
|
||||
|
||||
<% dialog.with_body do %>
|
||||
<p class="text-secondary text-sm mb-4">
|
||||
You are about to apply this rule to
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<%= link_to "Back to rules", rules_path %>
|
||||
|
||||
<%= render DialogComponent.new do |dialog| %>
|
||||
<%
|
||||
<%
|
||||
title = if @rule.name.present?
|
||||
"Edit #{@rule.resource_type} rule \"#{@rule.name}\""
|
||||
else
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
<div class="p-1">
|
||||
<div class="flex flex-col bg-container rounded-xl shadow-border-xs first_child:rounded-t-xl last_child:rounded-b-xl">
|
||||
<% @rules.each_with_index do |rule, idx| %>
|
||||
<%= render "rule", rule: rule%>
|
||||
<%= render "rule", rule: rule %>
|
||||
<% unless idx == @rules.size - 1 %>
|
||||
<div class="h-px bg-divider ml-4 mr-6"></div>
|
||||
<% end %>
|
||||
|
|
|
@ -1,31 +1,31 @@
|
|||
<%
|
||||
nav_sections = [
|
||||
{
|
||||
header: t('.general_section_title'),
|
||||
header: t(".general_section_title"),
|
||||
items: [
|
||||
{ label: t('.profile_label'), path: settings_profile_path, icon: 'circle-user' },
|
||||
{ label: t('.preferences_label'), path: settings_preferences_path, icon: 'bolt' },
|
||||
{ label: t('.security_label'), path: settings_security_path, icon: 'shield-check' },
|
||||
{ label: t('.self_hosting_label'), path: settings_hosting_path, icon: 'database', if: self_hosted? },
|
||||
{ label: t('.billing_label'), path: settings_billing_path, icon: 'circle-dollar-sign', if: !self_hosted? },
|
||||
{ label: t('.accounts_label'), path: accounts_path, icon: 'layers' },
|
||||
{ label: t('.imports_label'), path: imports_path, icon: 'download' }
|
||||
{ label: t(".profile_label"), path: settings_profile_path, icon: "circle-user" },
|
||||
{ label: t(".preferences_label"), path: settings_preferences_path, icon: "bolt" },
|
||||
{ label: t(".security_label"), path: settings_security_path, icon: "shield-check" },
|
||||
{ label: t(".self_hosting_label"), path: settings_hosting_path, icon: "database", if: self_hosted? },
|
||||
{ label: t(".billing_label"), path: settings_billing_path, icon: "circle-dollar-sign", if: !self_hosted? },
|
||||
{ label: t(".accounts_label"), path: accounts_path, icon: "layers" },
|
||||
{ label: t(".imports_label"), path: imports_path, icon: "download" }
|
||||
]
|
||||
},
|
||||
{
|
||||
header: t('.transactions_section_title'),
|
||||
header: t(".transactions_section_title"),
|
||||
items: [
|
||||
{ label: t('.tags_label'), path: tags_path, icon: 'tags' },
|
||||
{ label: t('.categories_label'), path: categories_path, icon: 'shapes' },
|
||||
{ label: t('.rules_label'), path: rules_path, icon: 'git-branch' },
|
||||
{ label: t('.merchants_label'), path: family_merchants_path, icon: 'store' }
|
||||
{ label: t(".tags_label"), path: tags_path, icon: "tags" },
|
||||
{ label: t(".categories_label"), path: categories_path, icon: "shapes" },
|
||||
{ label: t(".rules_label"), path: rules_path, icon: "git-branch" },
|
||||
{ label: t(".merchants_label"), path: family_merchants_path, icon: "store" }
|
||||
]
|
||||
},
|
||||
{
|
||||
header: t('.other_section_title'),
|
||||
header: t(".other_section_title"),
|
||||
items: [
|
||||
{ label: t('.whats_new_label'), path: changelog_path, icon: 'box' },
|
||||
{ label: t('.feedback_label'), path: feedback_path, icon: 'megaphone' }
|
||||
{ label: t(".whats_new_label"), path: changelog_path, icon: "box" },
|
||||
{ label: t(".feedback_label"), path: feedback_path, icon: "megaphone" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
<%# locals: (message:, id: nil) %>
|
||||
|
||||
<%= tag.div id: id, class: "flex gap-3 rounded-lg bg-container p-4 group w-full md:max-w-80 shadow-border-xs" do %>
|
||||
<div class="h-5 w-5 shrink-0 p-px text-primary">
|
||||
<%= icon "loader", class: "animate-pulse" %>
|
||||
</div>
|
||||
|
||||
<%= tag.p message, class: "text-primary text-sm font-medium" %>
|
||||
<% end %>
|
|
@ -4,9 +4,6 @@
|
|||
<div class="flex items-center gap-5">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= render MenuComponent.new do |menu| %>
|
||||
<% if Rails.env.development? %>
|
||||
<% menu.with_item(variant: "button", text: "Dev only: Sync all", href: sync_all_accounts_path, method: :post, icon: "refresh-cw") %>
|
||||
<% end %>
|
||||
<% menu.with_item(variant: "link", text: "New rule", href: new_rule_path(resource_type: "transaction"), icon: "plus", data: { turbo_frame: :modal }) %>
|
||||
<% menu.with_item(variant: "link", text: "Edit rules", href: rules_path, icon: "git-branch", data: { turbo_frame: :_top }) %>
|
||||
<% menu.with_item(variant: "link", text: "Edit categories", href: categories_path, icon: "shapes", data: { turbo_frame: :_top }) %>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue