From aef582f5537a76e4b33c1279edae7df5770ee2fd Mon Sep 17 00:00:00 2001 From: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:32:35 +0100 Subject: [PATCH] feat: Move upcoming recurring transactions in a dedicated tab (#771) * feat: Move upcoming transactions in a dedicated tab * Adjust formatting * feat: adjust visibility on mobile * feat: change translation label * feat: show only upcoming transactions expected in the next 10 days * feat: show upcoming transactions tab only when option enabled * feat: render empty partial when there are no recurring transactions * feat: align icon sizing and spacing between transactions and upcoming sections * feat: add missing localitazion labels * fix: move filter on upcoming transactions in controller * fix: add missing localitazion labels --- app/controllers/transactions_controller.rb | 4 +- .../recurring_transactions/_empty.html.erb | 4 + .../_projected_transaction.html.erb | 21 ++-- app/views/transactions/_list.html.erb | 53 ++++++++++ app/views/transactions/_upcoming.html.erb | 29 +++++ app/views/transactions/index.html.erb | 100 ++++-------------- .../views/recurring_transactions/en.yml | 5 +- config/locales/views/transactions/en.yml | 7 ++ 8 files changed, 129 insertions(+), 94 deletions(-) create mode 100644 app/views/recurring_transactions/_empty.html.erb create mode 100644 app/views/transactions/_list.html.erb create mode 100644 app/views/transactions/_upcoming.html.erb diff --git a/app/controllers/transactions_controller.rb b/app/controllers/transactions_controller.rb index a2210f3f6..0b775826d 100644 --- a/app/controllers/transactions_controller.rb +++ b/app/controllers/transactions_controller.rb @@ -24,11 +24,11 @@ class TransactionsController < ApplicationController @pagy, @transactions = pagy(base_scope, limit: safe_per_page) - # Load projected recurring transactions for next month + # Load projected recurring transactions for next 10 days @projected_recurring = Current.family.recurring_transactions .active .where("next_expected_date <= ? AND next_expected_date >= ?", - 1.month.from_now.to_date, + 10.days.from_now.to_date, Date.current) .includes(:merchant) end diff --git a/app/views/recurring_transactions/_empty.html.erb b/app/views/recurring_transactions/_empty.html.erb new file mode 100644 index 000000000..60c296e9b --- /dev/null +++ b/app/views/recurring_transactions/_empty.html.erb @@ -0,0 +1,4 @@ +
+

<%= t(".title") %>

+

<%= t(".description") %>

+
diff --git a/app/views/recurring_transactions/_projected_transaction.html.erb b/app/views/recurring_transactions/_projected_transaction.html.erb index 19a0eaa52..0f7b8b59d 100644 --- a/app/views/recurring_transactions/_projected_transaction.html.erb +++ b/app/views/recurring_transactions/_projected_transaction.html.erb @@ -1,19 +1,19 @@ <%# locals: (recurring_transaction:) %> -
-
+
+
- <%= content_tag :div, class: ["flex items-center gap-2"] do %> + <%= content_tag :div, class: ["flex items-center gap-3 lg:gap-4"] do %> <% if recurring_transaction.merchant.present? %> <% if recurring_transaction.merchant.logo_url.present? %> <%= image_tag Setting.transform_brand_fetch_url(recurring_transaction.merchant.logo_url), - class: "w-6 h-6 rounded-full", + class: "w-9 h-9 rounded-full", loading: "lazy" %> <% else %> <%= render DS::FilledIcon.new( variant: :text, text: recurring_transaction.merchant.name, - size: "sm", + size: "lg", rounded: true ) %> <% end %> @@ -21,27 +21,28 @@ <%= render DS::FilledIcon.new( variant: :text, text: recurring_transaction.name, - size: "sm", + size: "lg", rounded: true ) %> <% end %>
-
+
<%= recurring_transaction.merchant.present? ? recurring_transaction.merchant.name : recurring_transaction.name %>
- + <%= t("recurring_transactions.projected") %>
- <%= t("recurring_transactions.expected_on", date: l(recurring_transaction.next_expected_date, format: :short)) %> + <%= days_left = (recurring_transaction.next_expected_date.to_date - Time.zone.today).to_i + days_left.zero? ? t("recurring_transactions.expected_today") : t("recurring_transactions.expected_in", count: days_left) %>
@@ -53,7 +54,7 @@ <%= t("recurring_transactions.recurring") %>
-
+
<% display_amount = recurring_transaction.manual? && recurring_transaction.expected_amount_avg.present? ? recurring_transaction.expected_amount_avg : recurring_transaction.amount %> <%= content_tag :p, format_money(-Money.new(display_amount, recurring_transaction.currency)), class: ["font-medium", display_amount.negative? ? "text-success" : "text-subdued"] %>
diff --git a/app/views/transactions/_list.html.erb b/app/views/transactions/_list.html.erb new file mode 100644 index 000000000..1b0589e2a --- /dev/null +++ b/app/views/transactions/_list.html.erb @@ -0,0 +1,53 @@ +<%# locals: (transactions:, projected_recurring:, q:, pagy:) %> +
" + data-bulk-select-plural-label-value="<%= t(".transactions") %>" + class="flex flex-col bg-container rounded-xl shadow-border-xs px-3 py-4 lg:p-4 relative group"> + + <%= form_with url: imports_path, method: :post, class: "hidden", data: { drag_and_drop_import_target: "form" } do |f| %> + <%= f.hidden_field "import[type]", value: "TransactionImport" %> + <%= f.file_field "import[csv_file]", class: "hidden", data: { drag_and_drop_import_target: "input" }, accept: ".csv" %> + <% end %> + + <%= render "imports/drag_drop_overlay", title: t(".drag_drop_title"), subtitle: t(".drag_drop_subtitle") %> + + <%= render "transactions/searches/search" %> + + + + <% if @pagy.count > 0 || (@projected_recurring.any? && @q.blank?) %> +
+ <% if @transactions.any? %> +
+
+ <%= check_box_tag "selection_entry", + class: "checkbox checkbox--light hidden lg:block", + data: { + action: "bulk-select#togglePageSelection", + checkbox_toggle_target: "selectionEntry" + } %> +

transaction

+
+ + +

<%= t("transactions.show.amount") %>

+
+ <% end %> + +
+ <%= entries_by_date(@transactions.map(&:entry), totals: true) do |entries| %> + <%= render entries %> + <% end %> +
+
+ <% else %> + <%= render "entries/empty" %> + <% end %> + +
+ <%= render "shared/pagination", pagy: @pagy %> +
+
\ No newline at end of file diff --git a/app/views/transactions/_upcoming.html.erb b/app/views/transactions/_upcoming.html.erb new file mode 100644 index 000000000..894883ff7 --- /dev/null +++ b/app/views/transactions/_upcoming.html.erb @@ -0,0 +1,29 @@ +<% if @projected_recurring.any? %> +
+
+

<%= t("transactions.list.transaction") %>

+ +

<%= t("transactions.show.amount") %>

+
+
+ <% @projected_recurring.group_by(&:next_expected_date).sort.each do |date, transactions| %> +
+
+

+ <%= tag.span I18n.l(date, format: :long) %> + · + <%= tag.span transactions.size %> +

+
+
+ <% transactions.each do |recurring_transaction| %> + <%= render "recurring_transactions/projected_transaction", recurring_transaction: recurring_transaction %> + <% end %> +
+
+ <% end %> +
+
+<% else %> + <%= render "recurring_transactions/empty" %> +<% end %> \ No newline at end of file diff --git a/app/views/transactions/index.html.erb b/app/views/transactions/index.html.erb index 0cac7a7c8..671710c06 100644 --- a/app/views/transactions/index.html.erb +++ b/app/views/transactions/index.html.erb @@ -44,88 +44,26 @@ <%= render "summary", totals: @search.totals %> + <% if Current.family.recurring_transactions_disabled? %> + <%= render "transactions/list", transactions: @transactions, projected_recurring: @projected_recurring, q: params[:q], pagy: @pagy %> + <% else %> + <%= render DS::Tabs.new(active_tab: params[:tab].presence || "transactions") do |tabs| %> + <% tabs.with_nav(classes: "max-w-fit") do |nav| %> + <% nav.with_btn(id: "transactions", label: t("transactions.show.tab_transactions"), classes: "px-6") %> + <% nav.with_btn(id: "upcoming", label: t("transactions.show.tab_upcoming"), classes: "px-6") %> + <% end %> -
" - data-bulk-select-plural-label-value="<%= t(".transactions") %>" - class="flex flex-col bg-container rounded-xl shadow-border-xs px-3 py-4 lg:p-4 relative group"> - - <%= form_with url: imports_path, method: :post, class: "hidden", data: { drag_and_drop_import_target: "form" } do |f| %> - <%= f.hidden_field "import[type]", value: "TransactionImport" %> - <%= f.file_field "import[csv_file]", class: "hidden", data: { drag_and_drop_import_target: "input" }, accept: ".csv" %> - <% end %> - - <%= render "imports/drag_drop_overlay", title: "Drop CSV to import", subtitle: "Upload transactions directly" %> - - <%= render "transactions/searches/search" %> - - - - <% if @pagy.count > 0 || (@projected_recurring.any? && @q.blank?) %> -
- <% if @transactions.any? %> -
-
- <%= check_box_tag "selection_entry", - class: "checkbox checkbox--light hidden lg:block", - data: { - action: "bulk-select#togglePageSelection", - checkbox_toggle_target: "selectionEntry" - } %> -

transaction

-
- - -

amount

-
- <% end %> - -
- <% if @projected_recurring.any? && @q.blank? %> -
"> -
- -

<%= t("recurring_transactions.upcoming") %>

-
-
- <% @projected_recurring.group_by(&:next_expected_date).sort.each do |date, transactions| %> -
-
<%= l(date, format: :long) %>
- <% transactions.each do |recurring_transaction| %> - <%= render "recurring_transactions/projected_transaction", recurring_transaction: recurring_transaction %> - <% end %> -
- <% end %> -
-
- <% end %> - - <%= entries_by_date(@transactions.map(&:entry), totals: true) do |entries| %> - <%= render entries %> - <% end %> + <% tabs.with_panel(tab_id: "transactions") do %> +
+ <%= render "transactions/list", transactions: @transactions, projected_recurring: @projected_recurring, q: params[:q], pagy: @pagy %>
-
- <% else %> - <%= render "entries/empty" %> - <% end %> + <% end %> -
- <%= render "shared/pagination", pagy: @pagy %> -
-
+ <% tabs.with_panel(tab_id: "upcoming") do %> +
+ <%= render "transactions/upcoming" %> +
+ <% end %> + <% end %> + <% end %>
diff --git a/config/locales/views/recurring_transactions/en.yml b/config/locales/views/recurring_transactions/en.yml index 34749bc71..504d321d9 100644 --- a/config/locales/views/recurring_transactions/en.yml +++ b/config/locales/views/recurring_transactions/en.yml @@ -5,7 +5,10 @@ en: upcoming: Upcoming Recurring Transactions projected: Projected recurring: Recurring - expected_on: Expected on %{date} + expected_today: "Expected today" + expected_in: + one: "Expected in %{count} day" + other: "Expected in %{count} days" day_of_month: Day %{day} of month identify_patterns: Identify Patterns cleanup_stale: Clean Up Stale diff --git a/config/locales/views/transactions/en.yml b/config/locales/views/transactions/en.yml index 00078d609..1dfa2704f 100644 --- a/config/locales/views/transactions/en.yml +++ b/config/locales/views/transactions/en.yml @@ -49,6 +49,8 @@ en: overview: Overview settings: Settings tags_label: Tags + tab_transactions: Transactions + tab_upcoming: Upcoming uncategorized: "(uncategorized)" activity_labels: buy: Buy @@ -95,6 +97,11 @@ en: transaction: transaction transactions: transactions import: Import + list: + drag_drop_title: Drop CSV to import + drag_drop_subtitle: Upload transactions directly + transaction: transaction + transactions: transactions toggle_recurring_section: Toggle upcoming recurring transactions search: filters: