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

Implement savings rate insight card (#670)

This commit is contained in:
Josh Brown 2024-04-24 15:02:22 +01:00 committed by GitHub
parent 461fa672ff
commit b3f8ab78d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 89 additions and 54 deletions

View file

@ -10,10 +10,12 @@ class PagesController < ApplicationController
snapshot_transactions = Current.family.snapshot_transactions
@income_series = snapshot_transactions[:income_series]
@spending_series = snapshot_transactions[:spending_series]
@savings_rate_series = snapshot_transactions[:savings_rate_series]
snapshot_account_transactions = Current.family.snapshot_account_transactions
@top_spenders = snapshot_account_transactions[:top_spenders]
@top_earners = snapshot_account_transactions[:top_earners]
@top_savers = snapshot_account_transactions[:top_savers]
@account_groups = Current.family.accounts.by_group(period: @period, currency: Current.family.currency)
@transactions = Current.family.transactions.limit(5).order(date: :desc)
@ -22,7 +24,6 @@ class PagesController < ApplicationController
placeholder_series_data = 10.times.map do |i|
{ date: Date.current - i.days, value: Money.new(0) }
end
@savings_rate_series = TimeSeries.new(10.times.map { |i| { date: Date.current - i.days, value: 0 } })
@investing_series = TimeSeries.new(placeholder_series_data)
end

View file

@ -30,7 +30,6 @@ class Family < ApplicationRecord
def snapshot_account_transactions
period = Period.last_30_days
results = accounts.active.joins(:transactions)
.select(
"accounts.*",
@ -42,9 +41,16 @@ class Family < ApplicationRecord
.group("id")
.to_a
results.each do |r|
r.define_singleton_method(:savings_rate) do
(income - spending) / income
end
end
{
top_spenders: results.sort_by(&:spending).select { |a| a.spending > 0 }.reverse,
top_earners: results.sort_by(&:income).select { |a| a.income > 0 }.reverse
top_earners: results.sort_by(&:income).select { |a| a.income > 0 }.reverse,
top_savers: results.sort_by { |a| a.savings_rate }.reverse
}
end
@ -53,6 +59,7 @@ class Family < ApplicationRecord
spending = []
income = []
savings = []
rolling_totals.each do |r|
spending << {
date: r.date,
@ -63,11 +70,17 @@ class Family < ApplicationRecord
date: r.date,
value: Money.new(r.rolling_income, self.currency)
}
savings << {
date: r.date,
value: r.rolling_income != 0 ? (r.rolling_income - r.rolling_spend) / r.rolling_income : 0.to_d
}
end
{
income_series: TimeSeries.new(income, favorable_direction: "up"),
spending_series: TimeSeries.new(spending, favorable_direction: "down")
spending_series: TimeSeries.new(spending, favorable_direction: "down"),
savings_rate_series: TimeSeries.new(savings, favorable_direction: "up")
}
end

View file

@ -101,22 +101,38 @@
</div>
</div>
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
<div class="flex gap-4 h-full">
<div class="grow">
<%= render partial: "shared/value_heading", locals: {
label: t(".savings_rate"),
period: @period,
value: @savings_rate_series.last.value,
trend: @savings_rate_series.trend,
is_percentage: true
} %>
<div class="flex flex-col gap-4 h-full">
<div class="flex gap-4">
<div class="grow">
<%= render partial: "shared/value_heading", locals: {
label: t(".savings_rate"),
period: Period.last_30_days,
value: @savings_rate_series.last&.value,
trend: @savings_rate_series.trend,
is_percentage: true
} %>
</div>
<div
id="savingsRateChart"
class="h-full w-2/5"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= @savings_rate_series.to_json %>"
data-time-series-chart-use-labels-value="false"
data-time-series-chart-use-tooltip-value="false"></div>
</div>
<div class="flex gap-1.5">
<% @top_savers.first(3).each do |account| %>
<%= link_to account, class: "border border-alpha-black-25 rounded-full p-1 pr-2 flex items-center gap-1 text-xs text-gray-900 font-medium hover:bg-gray-25" do %>
<div class="flex items-center justify-center text-xs w-5 h-5 rounded-full <%= accountable_text_class(account.accountable_type) %> <%= accountable_bg_transparent_class(account.accountable_type) %>">
<%= account.name[0].upcase %>
</div>
<span><%= account.savings_rate > 0 ? "+" : "-" %><%= number_to_percentage(account.savings_rate.abs * 100, precision: 2) %></span>
<% end %>
<% end %>
<% if @top_savers.count > 3 %>
<div class="bg-gray-25 rounded-full flex h-full aspect-1 items-center justify-center text-xs font-medium text-gray-500">+<%= @top_savers.count - 3 %></div>
<% end %>
</div>
<div
id="savingsRateChart"
class="h-full w-2/5"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= @savings_rate_series.to_json %>"
data-time-series-chart-use-labels-value="false"></div>
</div>
</div>
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">

View file

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