@@ -3,6 +3,8 @@ class TransactionsController < ApplicationController
33
44 before_action :store_params! , only : :index
55
6+ require "digest/md5"
7+
68 def new
79 super
810 @income_categories = Current . family . categories . incomes . alphabetically
@@ -15,35 +17,91 @@ def index
1517
1618 set_focused_record ( transactions_query , params [ :focused_record_id ] , default_per_page : 50 )
1719
18- @pagy , @transactions = pagy (
19- transactions_query . includes (
20- { entry : :account } ,
21- :category , :merchant , :tags ,
22- transfer_as_outflow : { inflow_transaction : { entry : :account } } ,
23- transfer_as_inflow : { outflow_transaction : { entry : :account } }
24- ) . reverse_chronological ,
25- limit : params [ :per_page ] . presence || default_params [ :per_page ] ,
26- params : -> ( params ) { params . except ( :focused_record_id ) }
20+ # ------------------------------------------------------------------
21+ # Cache the expensive includes & pagination block so the DB work only
22+ # runs when either the query params change *or* any entry has been
23+ # updated for the current family.
24+ # ------------------------------------------------------------------
25+
26+ latest_update_ts = Current . family . entries . maximum ( :updated_at ) &.utc &.to_i || 0
27+
28+ items_per_page = ( params [ :per_page ] . presence || default_params [ :per_page ] ) . to_i
29+ items_per_page = 1 if items_per_page <= 0
30+
31+ current_page = ( params [ :page ] . presence || default_params [ :page ] ) . to_i
32+ current_page = 1 if current_page <= 0
33+
34+ # Build a compact cache digest: sanitized filters + page info + a
35+ # token that changes on updates *or* deletions.
36+ entries_changed_token = [ latest_update_ts , Current . family . entries . count ] . join ( ":" )
37+
38+ digest_source = {
39+ q : @q , # processed & sanitised search params
40+ page : current_page , # requested page number
41+ per : items_per_page , # page size
42+ tok : entries_changed_token
43+ } . to_json
44+
45+ cache_key = Current . family . build_cache_key (
46+ "transactions_idx_#{ Digest ::MD5 . hexdigest ( digest_source ) } "
2747 )
2848
29- # -------------------------------------------------------------------
30- # Cache totals
31- # -------------------------------------------------------------------
32- # Totals calculation is expensive (heavy SQL with grouping). We cache the
33- # result keyed by:
34- # • Family id
35- # • The family-level cache key that already embeds entries.maximum(:updated_at)
36- # • A digest of the current search params so each distinct filter set gets
37- # its own cache entry.
38- # When any entry is created/updated/deleted, the family cache key changes,
39- # automatically invalidating all related totals.
40-
41- params_digest = Digest ::MD5 . hexdigest ( @q . to_json )
42- cache_key = Current . family . build_cache_key ( "transactions_totals_#{ params_digest } " )
43-
44- @totals = Rails . cache . fetch ( cache_key ) do
45- Current . family . income_statement . totals ( transactions_scope : transactions_query )
49+ cache_data = Rails . cache . fetch ( cache_key , expires_in : 30 . minutes ) do
50+ current_page_i = current_page
51+
52+ # Initial query
53+ offset = ( current_page_i - 1 ) * items_per_page
54+ ids = transactions_query
55+ . reverse_chronological
56+ . limit ( items_per_page )
57+ . offset ( offset )
58+ . pluck ( :id )
59+
60+ total_count = transactions_query . count
61+
62+ if ids . empty? && total_count . positive? && current_page_i > 1
63+ current_page_i = ( total_count . to_f / items_per_page ) . ceil
64+ offset = ( current_page_i - 1 ) * items_per_page
65+
66+ ids = transactions_query
67+ . reverse_chronological
68+ . limit ( items_per_page )
69+ . offset ( offset )
70+ . pluck ( :id )
71+ end
72+
73+ { ids : ids , total_count : total_count , current_page : current_page_i }
4674 end
75+
76+ ids = cache_data [ :ids ]
77+ total_count = cache_data [ :total_count ]
78+ current_page = cache_data [ :current_page ]
79+
80+ # Build Pagy object (this part is cheap – done *after* potential
81+ # page fallback so the pagination UI reflects the adjusted page
82+ # number).
83+ @pagy = Pagy . new (
84+ count : total_count ,
85+ page : current_page ,
86+ items : items_per_page ,
87+ params : -> ( p ) { p . except ( :focused_record_id ) }
88+ )
89+
90+ # Fetch the transactions in the cached order
91+ @transactions = Current . family . transactions
92+ . active
93+ . where ( id : ids )
94+ . includes (
95+ { entry : :account } ,
96+ :category , :merchant , :tags ,
97+ transfer_as_outflow : { inflow_transaction : { entry : :account } } ,
98+ transfer_as_inflow : { outflow_transaction : { entry : :account } }
99+ )
100+
101+ # Preserve the order defined by `ids`
102+ @transactions = ids . map { |id | @transactions . detect { |t | t . id == id } } . compact
103+
104+ @totals = Current . family . income_statement . totals ( transactions_scope : transactions_query )
47105 end
48106
49107 def clear_filter
0 commit comments