mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-08 23:15:24 +02:00
Merge branch 'main' into fix-chat-title-edit
This commit is contained in:
commit
7b5661c948
6 changed files with 141 additions and 79 deletions
|
@ -26,7 +26,24 @@ class TransactionsController < ApplicationController
|
||||||
params: ->(params) { params.except(:focused_record_id) }
|
params: ->(params) { params.except(:focused_record_id) }
|
||||||
)
|
)
|
||||||
|
|
||||||
@totals = Current.family.income_statement.totals(transactions_scope: transactions_query)
|
# -------------------------------------------------------------------
|
||||||
|
# Cache totals
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Totals calculation is expensive (heavy SQL with grouping). We cache the
|
||||||
|
# result keyed by:
|
||||||
|
# • Family id
|
||||||
|
# • The family-level cache key that already embeds entries.maximum(:updated_at)
|
||||||
|
# • A digest of the current search params so each distinct filter set gets
|
||||||
|
# its own cache entry.
|
||||||
|
# When any entry is created/updated/deleted, the family cache key changes,
|
||||||
|
# automatically invalidating all related totals.
|
||||||
|
|
||||||
|
params_digest = Digest::MD5.hexdigest(@q.to_json)
|
||||||
|
cache_key = Current.family.build_cache_key("transactions_totals_#{params_digest}")
|
||||||
|
|
||||||
|
@totals = Rails.cache.fetch(cache_key) do
|
||||||
|
Current.family.income_statement.totals(transactions_scope: transactions_query)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_filter
|
def clear_filter
|
||||||
|
@ -150,6 +167,37 @@ class TransactionsController < ApplicationController
|
||||||
|
|
||||||
cleaned_params.delete(:amount_operator) unless cleaned_params[:amount].present?
|
cleaned_params.delete(:amount_operator) unless cleaned_params[:amount].present?
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Performance optimisation
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# When a user lands on the Transactions page without an explicit date
|
||||||
|
# filter, the previous behaviour queried *all* historical transactions
|
||||||
|
# for the family. For large datasets this results in very expensive
|
||||||
|
# SQL (as shown in Skylight) – particularly the aggregation queries
|
||||||
|
# used for @totals. To keep the UI responsive while still showing a
|
||||||
|
# sensible period of activity, we fall back to the user's preferred
|
||||||
|
# default period (stored on User#default_period, defaulting to
|
||||||
|
# "last_30_days") when **no** date filters have been supplied.
|
||||||
|
#
|
||||||
|
# This effectively changes the default view from "all-time" to a
|
||||||
|
# rolling window, dramatically reducing the rows scanned / grouped in
|
||||||
|
# Postgres without impacting the UX (the user can always clear the
|
||||||
|
# filter).
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
if cleaned_params[:start_date].blank? && cleaned_params[:end_date].blank?
|
||||||
|
period_key = Current.user&.default_period.presence || "last_30_days"
|
||||||
|
|
||||||
|
begin
|
||||||
|
period = Period.from_key(period_key)
|
||||||
|
cleaned_params[:start_date] = period.start_date
|
||||||
|
cleaned_params[:end_date] = period.end_date
|
||||||
|
rescue Period::InvalidKeyError
|
||||||
|
# Fallback – should never happen but keeps things safe.
|
||||||
|
cleaned_params[:start_date] = 30.days.ago.to_date
|
||||||
|
cleaned_params[:end_date] = Date.current
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
cleaned_params
|
cleaned_params
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ class BalanceSheet
|
||||||
end
|
end
|
||||||
|
|
||||||
def classification_groups
|
def classification_groups
|
||||||
|
Rails.cache.fetch(family.build_cache_key("bs_classification_groups")) do
|
||||||
asset_groups = account_groups("asset")
|
asset_groups = account_groups("asset")
|
||||||
liability_groups = account_groups("liability")
|
liability_groups = account_groups("liability")
|
||||||
|
|
||||||
|
@ -44,10 +45,13 @@ class BalanceSheet
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def account_groups(classification = nil)
|
def account_groups(classification = nil)
|
||||||
|
Rails.cache.fetch(family.build_cache_key("bs_account_groups_#{classification || 'all'}")) do
|
||||||
classification_accounts = classification ? totals_query.filter { |t| t.classification == classification } : totals_query
|
classification_accounts = classification ? totals_query.filter { |t| t.classification == classification } : totals_query
|
||||||
classification_total = classification_accounts.sum(&:converted_balance)
|
classification_total = classification_accounts.sum(&:converted_balance)
|
||||||
|
|
||||||
account_groups = classification_accounts.group_by(&:accountable_type)
|
account_groups = classification_accounts.group_by(&:accountable_type)
|
||||||
.transform_keys { |k| Accountable.from_type(k) }
|
.transform_keys { |k| Accountable.from_type(k) }
|
||||||
|
|
||||||
|
@ -56,7 +60,7 @@ class BalanceSheet
|
||||||
|
|
||||||
key = accountable.model_name.param_key
|
key = accountable.model_name.param_key
|
||||||
|
|
||||||
AccountGroup.new(
|
group = AccountGroup.new(
|
||||||
id: classification ? "#{classification}_#{key}_group" : "#{key}_group",
|
id: classification ? "#{classification}_#{key}_group" : "#{key}_group",
|
||||||
key: key,
|
key: key,
|
||||||
name: accountable.display_name,
|
name: accountable.display_name,
|
||||||
|
@ -68,13 +72,11 @@ class BalanceSheet
|
||||||
color: accountable.color,
|
color: accountable.color,
|
||||||
syncing?: accounts.any?(&:is_syncing),
|
syncing?: accounts.any?(&:is_syncing),
|
||||||
accounts: accounts.map do |account|
|
accounts: accounts.map do |account|
|
||||||
account.define_singleton_method(:weight) do
|
|
||||||
classification_total.zero? ? 0 : account.converted_balance / classification_total.to_d * 100
|
|
||||||
end
|
|
||||||
|
|
||||||
account
|
account
|
||||||
end.sort_by(&:weight).reverse
|
end.sort_by(&:converted_balance).reverse
|
||||||
)
|
)
|
||||||
|
|
||||||
|
group
|
||||||
end
|
end
|
||||||
|
|
||||||
groups.sort_by do |group|
|
groups.sort_by do |group|
|
||||||
|
@ -83,6 +85,7 @@ class BalanceSheet
|
||||||
manual_order.index(type_name) || Float::INFINITY
|
manual_order.index(type_name) || Float::INFINITY
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def net_worth_series(period: Period.last_30_days)
|
def net_worth_series(period: Period.last_30_days)
|
||||||
active_accounts.balance_series(currency: currency, period: period, favorable_direction: "up")
|
active_accounts.balance_series(currency: currency, period: period, favorable_direction: "up")
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<%= turbo_frame_tag "#{@accountable.model_name.param_key}_sparkline" do %>
|
<% cache Current.family.build_cache_key("#{@accountable.name}_sparkline_html") do %>
|
||||||
|
<%= turbo_frame_tag "#{@accountable.model_name.param_key}_sparkline" do %>
|
||||||
<div class="flex items-center justify-end gap-1">
|
<div class="flex items-center justify-end gap-1">
|
||||||
<div class="w-8 h-3">
|
<div class="w-8 h-3">
|
||||||
<%= render "shared/sparkline", id: dom_id(@accountable, :sparkline_chart), series: @series %>
|
<%= render "shared/sparkline", id: dom_id(@accountable, :sparkline_chart), series: @series %>
|
||||||
|
@ -8,4 +9,5 @@
|
||||||
style: "color: #{@series.trend.color}",
|
style: "color: #{@series.trend.color}",
|
||||||
class: "font-mono text-right text-xs font-medium text-primary" %>
|
class: "font-mono text-right text-xs font-medium text-primary" %>
|
||||||
</div>
|
</div>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<%= turbo_frame_tag dom_id(@account, :sparkline) do %>
|
<% cache Current.family.build_cache_key("account_#{@account.id}_sparkline_html") do %>
|
||||||
|
<%= turbo_frame_tag dom_id(@account, :sparkline) do %>
|
||||||
<div class="flex items-center justify-end gap-1">
|
<div class="flex items-center justify-end gap-1">
|
||||||
<div class="w-8 h-5">
|
<div class="w-8 h-5">
|
||||||
<%= render "shared/sparkline", id: dom_id(@account, :sparkline_chart), series: @account.sparkline_series %>
|
<%= render "shared/sparkline", id: dom_id(@account, :sparkline_chart), series: @account.sparkline_series %>
|
||||||
|
@ -8,4 +9,5 @@
|
||||||
style: "color: #{@account.sparkline_series.trend.color}",
|
style: "color: #{@account.sparkline_series.trend.color}",
|
||||||
class: "font-mono text-right text-xs font-medium text-primary" %>
|
class: "font-mono text-right text-xs font-medium text-primary" %>
|
||||||
</div>
|
</div>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -116,7 +116,12 @@
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="ml-auto flex items-center text-right gap-6">
|
<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="w-28 shrink-0 flex items-center justify-end gap-2">
|
||||||
<%= render "pages/dashboard/group_weight", weight: account.weight, color: account_group.color %>
|
<%
|
||||||
|
# Calculate weight as percentage of classification total
|
||||||
|
classification_total = classification_group.total_money.amount
|
||||||
|
account_weight = classification_total.zero? ? 0 : account.converted_balance / classification_total * 100
|
||||||
|
%>
|
||||||
|
<%= render "pages/dashboard/group_weight", weight: account_weight, color: account_group.color %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-40 shrink-0">
|
<div class="w-40 shrink-0">
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
<%# locals: (weight:, color:) %>
|
<%# locals: (weight:, color:) %>
|
||||||
|
|
||||||
|
<% effective_weight = weight.presence || 0 %>
|
||||||
|
|
||||||
<div class="w-full flex items-center justify-between gap-2">
|
<div class="w-full flex items-center justify-between gap-2">
|
||||||
<div class="flex gap-[3px]">
|
<div class="flex gap-[3px]">
|
||||||
<% 10.times do |i| %>
|
<% 10.times do |i| %>
|
||||||
<div class="w-0.5 h-2.5 rounded-lg <%= i < (weight / 10.0).ceil ? "" : "opacity-20" %>" style="background-color: <%= color %>;"></div>
|
<div class="w-0.5 h-2.5 rounded-lg <%= i < (effective_weight / 10.0).ceil ? "" : "opacity-20" %>" style="background-color: <%= color %>;"></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm"><%= number_to_percentage(weight, precision: 2) %></p>
|
<p class="text-sm"><%= number_to_percentage(effective_weight, precision: 2) %></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue