1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-02 20:15:22 +02:00

Add the ability to "rollup" values in a time series (#554)

* Clean up time series models

* Add value group rollup class for summarizing hierarchical data

* Integrate new classes

* Update UI to use new patterns

* Update D3 charts to expect new data format

* Clean up account model

* More cleanup

* Money improvements

* Use new money fields

* Remove invalid fixture data to avoid orphaned accountables

* Update time series to work better with collections

* Fix tests and UI bugs
This commit is contained in:
Zach Gollwitzer 2024-03-19 09:10:40 -04:00 committed by GitHub
parent 0a8518506c
commit f904d9d062
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 687 additions and 391 deletions

View file

@ -1,8 +1,7 @@
<%# locals: (valuation_series:) %>
<% valuation_series.data.reverse_each.with_index do |valuation_item, index| %>
<% valuation, trend = valuation_item.values_at(:raw, :trend) %>
<% valuation_styles = trend_styles(trend) %>
<%= turbo_frame_tag dom_id(valuation) do %>
<% valuation_series.values.reverse_each.with_index do |valuation, index| %>
<% valuation_styles = trend_styles(valuation.trend) %>
<%= turbo_frame_tag dom_id(valuation.original) do %>
<div class="p-4 flex items-center">
<div class="w-16">
<div class="w-8 h-8 rounded-full p-1.5 flex items-center justify-center <%= valuation_styles[:bg_class] %>">
@ -15,14 +14,14 @@
<%# TODO: Add descriptive name of valuation %>
<p class="text-gray-500">Manually entered</p>
</div>
<div class="flex text-sm font-medium text-right"><%= format_money valuation.value_money %></div>
<div class="flex text-sm font-medium text-right"><%= format_money valuation.value %></div>
</div>
<div class="flex w-56 justify-end text-right text-sm font-medium">
<% if trend.amount == 0 %>
<% if valuation.trend.value == 0 %>
<span class="text-gray-500">No change</span>
<% else %>
<span class="<%= valuation_styles[:text_class] %>"><%= valuation_styles[:symbol] %><%= format_money trend.amount.abs %></span>
<span class="<%= valuation_styles[:text_class] %>">(<%= lucide_icon(valuation_styles[:icon], class: "w-4 h-4 align-text-bottom inline") %> <%= trend.percent %>%)</span>
<span class="<%= valuation_styles[:text_class] %>"><%= valuation_styles[:symbol] %><%= format_money valuation.trend.value.abs %></span>
<span class="<%= valuation_styles[:text_class] %>">(<%= lucide_icon(valuation_styles[:icon], class: "w-4 h-4 align-text-bottom inline") %> <%= valuation.trend.percent %>%)</span>
<% end %>
</div>
<div class="relative w-[72px]" data-controller="dropdown">
@ -30,18 +29,18 @@
<%= lucide_icon("more-horizontal", class: "w-5 h-5 text-gray-500" ) %>
</button>
<div class="hidden absolute min-w-[200px] z-10 top-10 right-0 bg-white p-1 rounded-sm shadow-xs border border-alpha-black-25 w-fit" data-dropdown-target="menu">
<%= link_to edit_valuation_path(valuation), class: "flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
<%= link_to edit_valuation_path(valuation.original), class: "flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
<%= lucide_icon("pencil-line", class: "w-5 h-5 text-gray-500 shrink-0") %>
<span class="text-gray-900 text-sm">Edit entry</span>
<% end %>
<%= link_to valuation_path(valuation), data: { turbo_method: :delete, turbo_confirm: "Are you sure?" }, class: "text-red-600 flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
<%= link_to valuation_path(valuation.original), data: { turbo_method: :delete, turbo_confirm: "Are you sure?" }, class: "text-red-600 flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
<%= lucide_icon("trash-2", class: "w-5 h-5 shrink-0") %>
<span class="text-sm">Delete entry</span>
<% end %>
</div>
</div>
</div>
<% unless index == valuation_series.data.size - 1 %>
<% unless index == valuation_series.values.size - 1 %>
<div class="h-px bg-alpha-black-50 ml-20 mr-4"></div>
<% end %>
<% end %>

View file

@ -1,5 +1,4 @@
<%= turbo_stream_from @account %>
<% balance = Money.new(@account.balance, @account.currency) %>
<div class="space-y-4">
<div class="flex justify-between items-center">
<div class="flex items-center gap-3">
@ -11,7 +10,7 @@
<div class="flex items-center gap-3">
<div class="relative cursor-not-allowed">
<div class="flex items-center gap-2 px-3 py-2">
<span class="text-gray-900"><%= balance.currency.iso_code %> <%= balance.currency.symbol %></span>
<span class="text-gray-900"><%= @account.balance_money.currency.iso_code %> <%= @account.balance_money.currency.symbol %></span>
<%= lucide_icon("chevron-down", class: "w-5 h-5 text-gray-500") %>
</div>
</div>

View file

@ -1,48 +1,47 @@
<%# locals: (account_group:) %>
<% accountable_type, account_details = account_group%>
<% text_class = accountable_text_class(accountable_type) %>
<% text_class = accountable_text_class(account_group.name) %>
<details class="open:bg-gray-25 group">
<summary class="flex p-4 items-center w-full rounded-lg font-medium hover:bg-gray-50 text-gray-500 text-sm font-medium cursor-pointer">
<%= lucide_icon("chevron-down", class: "hidden group-open:block w-5 h-5") %>
<%= lucide_icon("chevron-right", class: "group-open:hidden w-5 h-5") %>
<div class="ml-4 h-2.5 w-2.5 rounded-full <%= accountable_bg_class(accountable_type) %>"></div>
<p class="text-gray-900 ml-2"><%= to_accountable_title(Accountable.from_type(accountable_type)) %></p>
<div class="ml-4 h-2.5 w-2.5 rounded-full <%= accountable_bg_class(account_group.name) %>"></div>
<p class="text-gray-900 ml-2"><%= to_accountable_title(Accountable.from_type(account_group.name)) %></p>
<span class="mx-1">&middot;</span>
<div ><%= account_details[:accounts].size %></div>
<div ><%= account_group.children.count %></div>
<div class="ml-auto text-right flex items-center gap-10 text-sm font-medium text-gray-900">
<div class="flex items-center justify-end gap-2 w-24">
<%= render partial: "shared/progress_circle", locals: { progress: account_details[:allocation], text_class: text_class } %>
<p><%= account_details[:allocation] %>%</p>
<%= render partial: "shared/progress_circle", locals: { progress: account_group.percent_of_total, text_class: text_class } %>
<p><%= account_group.percent_of_total.round(1) %>%</p>
</div>
<div class="w-24">
<p><%= format_money account_details[:end_balance] %></p>
<p><%= format_money account_group.sum %></p>
</div>
<div class="w-40">
<%= render partial: "shared/trend_change", locals: { trend: account_details[:trend] } %>
<%= render partial: "shared/trend_change", locals: { trend: account_group.series.trend } %>
</div>
</div>
</summary>
<div class="px-4 py-3 space-y-4">
<% account_details[:accounts].map do |account| %>
<% account_group.children.map do |account| %>
<div class="flex items-center justify-between text-sm font-medium text-gray-900">
<div class="flex items-center gap-4">
<div class="flex items-center justify-center w-8 h-8 rounded-full <%= text_class %> <%= accountable_bg_transparent_class(accountable_type) %>">
<%= account[:name][0].upcase %>
<div class="flex items-center justify-center w-8 h-8 rounded-full <%= text_class %> <%= accountable_bg_transparent_class(account_group.name) %>">
<%= account.name[0].upcase %>
</div>
<div>
<p><%= account[:name] %></p>
<p><%= account.name %></p>
</div>
</div>
<div class="flex gap-10 items-center text-right">
<div class="flex items-center justify-end gap-2 w-24">
<%= render partial: "shared/progress_circle", locals: { progress: account[:allocation], text_class: text_class } %>
<p><%= account[:allocation] %>%</p>
<%= render partial: "shared/progress_circle", locals: { progress: account.percent_of_total, text_class: text_class } %>
<p><%= account.percent_of_total %>%</p>
</div>
<div class="w-24">
<p><%= format_money account[:end_balance] %></p>
<p><%= format_money account.sum %></p>
</div>
<div class="w-40">
<%= render partial: "shared/trend_change", locals: { trend: account[:trend] } %>
<%= render partial: "shared/trend_change", locals: { trend: account.series.trend } %>
</div>
</div>
</div>

View file

@ -1,16 +1,16 @@
<%# locals: (account_groups:) %>
<div class="space-y-4">
<div class="flex gap-1">
<% account_groups.each do |type, value| %>
<div class="h-1.5 rounded-sm w-12 <%= accountable_bg_class(type) %>" style="width: <%= value[:allocation] %>%;"></div>
<% account_groups.each do |group| %>
<div class="h-1.5 rounded-sm w-12 <%= accountable_bg_class(group.name) %>" style="width: <%= group.percent_of_total %>%;"></div>
<% end %>
</div>
<div class="flex gap-4">
<% account_groups.each do |type, value| %>
<% account_groups.each do |group| %>
<div class="flex items-center gap-2 text-sm">
<div class="h-2.5 w-2.5 rounded-full <%= accountable_bg_class(type) %>"></div>
<p class="text-gray-500"><%= to_accountable_title(Accountable.from_type(type)) %></p>
<p class="text-black"><%= value[:allocation] %>%</p>
<div class="h-2.5 w-2.5 rounded-full <%= accountable_bg_class(group.name) %>"></div>
<p class="text-gray-500"><%= to_accountable_title(Accountable.from_type(group.name)) %></p>
<p class="text-black"><%= group.percent_of_total.round(1) %>%</p>
</div>
<% end %>
</div>

View file

@ -34,7 +34,7 @@
data-controller="trendline"
id="assetsTrendline"
class="h-full w-2/5"
data-trendline-series-value="<%= @asset_series.serialize_for_d3_chart %>"
data-trendline-series-value="<%= @asset_series.to_json %>"
data-trendline-classification-value="asset"
></div>
</div>
@ -52,7 +52,7 @@
data-controller="trendline"
id="liabilitiesTrendline"
class="h-full w-2/5"
data-trendline-series-value="<%= @liability_series.serialize_for_d3_chart %>"
data-trendline-series-value="<%= @liability_series.to_json %>"
data-trendline-classification-value="liability"
></div>
</div>
@ -75,12 +75,12 @@
</div>
<div>
<div data-tabs-target="tab" id="asset-summary-tab" class="space-y-6">
<%= render partial: "account_percentages_bar", locals: { account_groups: @account_groups[:asset][:groups] } %>
<%= render partial: "account_percentages_table", locals: { account_groups: @account_groups[:asset][:groups] } %>
<%= render partial: "account_percentages_bar", locals: { account_groups: @account_groups[:assets].children } %>
<%= render partial: "account_percentages_table", locals: { account_groups: @account_groups[:assets].children } %>
</div>
<div data-tabs-target="tab" id="liability-summary-tab" class="space-y-6 hidden">
<%= render partial: "account_percentages_bar", locals: { account_groups: @account_groups[:liability][:groups] } %>
<%= render partial: "account_percentages_table", locals: { account_groups: @account_groups[:liability][:groups] } %>
<%= render partial: "account_percentages_bar", locals: { account_groups: @account_groups[:liabilities].children } %>
<%= render partial: "account_percentages_table", locals: { account_groups: @account_groups[:liabilities].children } %>
</div>
</div>
</div>

View file

@ -1,6 +1,6 @@
<%# locals: (series:) %>
<% if series %>
<div data-controller="line-chart" id="lineChart" class="w-full h-full" data-line-chart-series-value="<%= series.serialize_for_d3_chart %>"></div>
<div data-controller="line-chart" id="lineChart" class="w-full h-full" data-line-chart-series-value="<%= series.to_json %>"></div>
<% else %>
<div class="w-full h-full flex items-center justify-center">
<p class="text-gray-500">No data available for the selected period.</p>

View file

@ -4,7 +4,7 @@
<% if trend.direction == "flat" %>
<span>No change</span>
<% else %>
<span><%= styles[:symbol] %><%= format_money trend.amount.abs %></span>
<span><%= styles[:symbol] %><%= format_money trend.value.abs %></span>
<span>(<%= lucide_icon(styles[:icon], class: "w-4 h-4 align-text-bottom inline") %><%= trend.percent %>%)</span>
<% end %>
</p>