mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 14:31:25 +00:00
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
This commit is contained in:
@@ -24,11 +24,11 @@ class TransactionsController < ApplicationController
|
|||||||
|
|
||||||
@pagy, @transactions = pagy(base_scope, limit: safe_per_page)
|
@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
|
@projected_recurring = Current.family.recurring_transactions
|
||||||
.active
|
.active
|
||||||
.where("next_expected_date <= ? AND next_expected_date >= ?",
|
.where("next_expected_date <= ? AND next_expected_date >= ?",
|
||||||
1.month.from_now.to_date,
|
10.days.from_now.to_date,
|
||||||
Date.current)
|
Date.current)
|
||||||
.includes(:merchant)
|
.includes(:merchant)
|
||||||
end
|
end
|
||||||
|
|||||||
4
app/views/recurring_transactions/_empty.html.erb
Normal file
4
app/views/recurring_transactions/_empty.html.erb
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<div class="flex flex-col items-center justify-center py-40">
|
||||||
|
<p class="text-secondary mb-2"><%= t(".title") %></p>
|
||||||
|
<p class="text-subdued max-w-xs text-center"><%= t(".description") %></p>
|
||||||
|
</div>
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
<%# locals: (recurring_transaction:) %>
|
<%# locals: (recurring_transaction:) %>
|
||||||
|
|
||||||
<div class="grid grid-cols-12 items-center text-subdued text-sm font-medium p-4 lg:p-4 bg-container-inset rounded">
|
<div class="group flex lg:grid lg:grid-cols-12 items-center text-primary text-sm font-medium p-3 lg:p-4">
|
||||||
<div class="pr-4 lg:pr-10 flex items-center gap-3 lg:gap-4 col-span-8">
|
<div class="pr-4 lg:pr-10 flex items-center gap-3 lg:gap-4 col-span-8 min-w-0">
|
||||||
<div class="max-w-full">
|
<div class="max-w-full">
|
||||||
<%= 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.present? %>
|
||||||
<% if recurring_transaction.merchant.logo_url.present? %>
|
<% if recurring_transaction.merchant.logo_url.present? %>
|
||||||
<%= image_tag Setting.transform_brand_fetch_url(recurring_transaction.merchant.logo_url),
|
<%= 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" %>
|
loading: "lazy" %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= render DS::FilledIcon.new(
|
<%= render DS::FilledIcon.new(
|
||||||
variant: :text,
|
variant: :text,
|
||||||
text: recurring_transaction.merchant.name,
|
text: recurring_transaction.merchant.name,
|
||||||
size: "sm",
|
size: "lg",
|
||||||
rounded: true
|
rounded: true
|
||||||
) %>
|
) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -21,27 +21,28 @@
|
|||||||
<%= render DS::FilledIcon.new(
|
<%= render DS::FilledIcon.new(
|
||||||
variant: :text,
|
variant: :text,
|
||||||
text: recurring_transaction.name,
|
text: recurring_transaction.name,
|
||||||
size: "sm",
|
size: "lg",
|
||||||
rounded: true
|
rounded: true
|
||||||
) %>
|
) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="truncate">
|
<div class="truncate">
|
||||||
<div class="space-y-0.5">
|
<div class="space-y-0.5">
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
<div class="flex items-center gap-1 min-w-0">
|
||||||
<div class="truncate flex-shrink">
|
<div class="truncate flex-shrink">
|
||||||
<%= recurring_transaction.merchant.present? ? recurring_transaction.merchant.name : recurring_transaction.name %>
|
<%= recurring_transaction.merchant.present? ? recurring_transaction.merchant.name : recurring_transaction.name %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-1 flex-shrink-0">
|
<div class="flex items-center gap-1 flex-shrink-0">
|
||||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-tint-10 text-link">
|
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-tint-10 text-link">
|
||||||
<%= t("recurring_transactions.projected") %>
|
<%= t("recurring_transactions.projected") %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-secondary text-xs font-normal">
|
<div class="text-secondary text-xs font-normal">
|
||||||
<%= 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) %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,7 +54,7 @@
|
|||||||
<span class="text-xs text-secondary"><%= t("recurring_transactions.recurring") %></span>
|
<span class="text-xs text-secondary"><%= t("recurring_transactions.recurring") %></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-span-2 ml-auto text-right">
|
<div class="col-span-2 ml-auto text-right shrink-0">
|
||||||
<% display_amount = recurring_transaction.manual? && recurring_transaction.expected_amount_avg.present? ? recurring_transaction.expected_amount_avg : recurring_transaction.amount %>
|
<% 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"] %>
|
<%= content_tag :p, format_money(-Money.new(display_amount, recurring_transaction.currency)), class: ["font-medium", display_amount.negative? ? "text-success" : "text-subdued"] %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
53
app/views/transactions/_list.html.erb
Normal file
53
app/views/transactions/_list.html.erb
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<%# locals: (transactions:, projected_recurring:, q:, pagy:) %>
|
||||||
|
<div id="transactions"
|
||||||
|
data-controller="bulk-select checkbox-toggle drag-and-drop-import"
|
||||||
|
data-bulk-select-singular-label-value="<%= t(".transaction") %>"
|
||||||
|
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" %>
|
||||||
|
|
||||||
|
<div id="entry-selection-bar" data-bulk-select-target="selectionBar" class="flex justify-center hidden">
|
||||||
|
<%= render "transactions/selection_bar" %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if @pagy.count > 0 || (@projected_recurring.any? && @q.blank?) %>
|
||||||
|
<div class="grow overflow-y-auto">
|
||||||
|
<% if @transactions.any? %>
|
||||||
|
<div class="grid-cols-12 bg-container-inset rounded-xl px-5 py-3 text-xs uppercase font-medium text-secondary items-center mb-4 grid">
|
||||||
|
<div class="pl-0.5 col-span-8 flex items-center gap-4">
|
||||||
|
<%= check_box_tag "selection_entry",
|
||||||
|
class: "checkbox checkbox--light hidden lg:block",
|
||||||
|
data: {
|
||||||
|
action: "bulk-select#togglePageSelection",
|
||||||
|
checkbox_toggle_target: "selectionEntry"
|
||||||
|
} %>
|
||||||
|
<p>transaction</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="col-span-2 md:block hidden"><%= t("transactions.form.category_label") %></p>
|
||||||
|
<p class="col-span-2 col-start-11 md:col-start-auto justify-self-end md:block"><%= t("transactions.show.amount") %></p>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
<%= entries_by_date(@transactions.map(&:entry), totals: true) do |entries| %>
|
||||||
|
<%= render entries %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<%= render "entries/empty" %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="pt-4">
|
||||||
|
<%= render "shared/pagination", pagy: @pagy %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
29
app/views/transactions/_upcoming.html.erb
Normal file
29
app/views/transactions/_upcoming.html.erb
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<% if @projected_recurring.any? %>
|
||||||
|
<div id="upcoming" class="flex flex-col bg-container rounded-xl shadow-border-xs px-3 py-4 lg:p-4 relative group">
|
||||||
|
<div class="grid-cols-12 bg-container-inset rounded-xl px-5 py-3 text-xs uppercase font-medium text-secondary items-center mb-4 grid">
|
||||||
|
<div class="pl-0.5 col-span-8 flex items-center gap-4"><p><%= t("transactions.list.transaction") %></p></div>
|
||||||
|
<p class="col-span-2 md:block hidden"><%= t("transactions.search.filters.type") %></p>
|
||||||
|
<p class="col-span-2 col-start-11 md:col-start-auto justify-self-end md:block"><%= t("transactions.show.amount") %></p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<% @projected_recurring.group_by(&:next_expected_date).sort.each do |date, transactions| %>
|
||||||
|
<div id="upcoming-group-<%= date %>" class="bg-container-inset rounded-xl p-1 w-full">
|
||||||
|
<div class="py-2 px-4 flex items-center justify-between font-medium text-xs text-secondary">
|
||||||
|
<p class="uppercase space-x-1.5">
|
||||||
|
<%= tag.span I18n.l(date, format: :long) %>
|
||||||
|
<span>·</span>
|
||||||
|
<%= tag.span transactions.size %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-container shadow-border-xs rounded-lg">
|
||||||
|
<% transactions.each do |recurring_transaction| %>
|
||||||
|
<%= render "recurring_transactions/projected_transaction", recurring_transaction: recurring_transaction %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<%= render "recurring_transactions/empty" %>
|
||||||
|
<% end %>
|
||||||
@@ -44,88 +44,26 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<%= render "summary", totals: @search.totals %>
|
<%= 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 %>
|
||||||
|
|
||||||
<div id="transactions"
|
<% tabs.with_panel(tab_id: "transactions") do %>
|
||||||
data-controller="bulk-select checkbox-toggle drag-and-drop-import"
|
<div class="bg-container rounded-xl shadow-border-xs">
|
||||||
data-bulk-select-singular-label-value="<%= t(".transaction") %>"
|
<%= render "transactions/list", transactions: @transactions, projected_recurring: @projected_recurring, q: params[:q], pagy: @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: "Drop CSV to import", subtitle: "Upload transactions directly" %>
|
|
||||||
|
|
||||||
<%= render "transactions/searches/search" %>
|
|
||||||
|
|
||||||
<div id="entry-selection-bar" data-bulk-select-target="selectionBar" class="flex justify-center hidden">
|
|
||||||
<%= render "transactions/selection_bar" %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% if @pagy.count > 0 || (@projected_recurring.any? && @q.blank?) %>
|
|
||||||
<div class="grow overflow-y-auto">
|
|
||||||
<% if @transactions.any? %>
|
|
||||||
<div class="grid-cols-12 bg-container-inset rounded-xl px-5 py-3 text-xs uppercase font-medium text-secondary items-center mb-4 grid">
|
|
||||||
<div class="pl-0.5 col-span-8 flex items-center gap-4">
|
|
||||||
<%= check_box_tag "selection_entry",
|
|
||||||
class: "checkbox checkbox--light hidden lg:block",
|
|
||||||
data: {
|
|
||||||
action: "bulk-select#togglePageSelection",
|
|
||||||
checkbox_toggle_target: "selectionEntry"
|
|
||||||
} %>
|
|
||||||
<p>transaction</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="col-span-2 md:block hidden">category</p>
|
|
||||||
<p class="col-span-2 col-start-11 md:col-start-auto justify-self-end md:block">amount</p>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class="space-y-6">
|
|
||||||
<% if @projected_recurring.any? && @q.blank? %>
|
|
||||||
<div
|
|
||||||
class="space-y-2"
|
|
||||||
data-controller="transactions-section"
|
|
||||||
data-transactions-section-section-key-value="upcoming_recurring"
|
|
||||||
data-transactions-section-collapsed-value="<%= Current.user.transactions_section_collapsed?("upcoming_recurring") %>">
|
|
||||||
<div class="flex items-center gap-2 px-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="text-secondary hover:text-primary transition-colors p-0.5"
|
|
||||||
data-action="click->transactions-section#toggle keydown->transactions-section#handleToggleKeydown"
|
|
||||||
data-transactions-section-target="button"
|
|
||||||
aria-label="<%= t("transactions.toggle_recurring_section") %>"
|
|
||||||
aria-expanded="<%= !Current.user.transactions_section_collapsed?("upcoming_recurring") %>">
|
|
||||||
<%= icon("chevron-down", size: "sm", class: "transition-transform", data: { transactions_section_target: "chevron" }) %>
|
|
||||||
</button>
|
|
||||||
<h3 class="text-xs uppercase font-medium text-secondary"><%= t("recurring_transactions.upcoming") %></h3>
|
|
||||||
</div>
|
|
||||||
<div data-transactions-section-target="content">
|
|
||||||
<% @projected_recurring.group_by(&:next_expected_date).sort.each do |date, transactions| %>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="text-xs text-secondary px-2 pt-2"><%= l(date, format: :long) %></div>
|
|
||||||
<% transactions.each do |recurring_transaction| %>
|
|
||||||
<%= render "recurring_transactions/projected_transaction", recurring_transaction: recurring_transaction %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%= entries_by_date(@transactions.map(&:entry), totals: true) do |entries| %>
|
|
||||||
<%= render entries %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<% end %>
|
||||||
<% else %>
|
|
||||||
<%= render "entries/empty" %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class="pt-4">
|
<% tabs.with_panel(tab_id: "upcoming") do %>
|
||||||
<%= render "shared/pagination", pagy: @pagy %>
|
<div class="bg-container rounded-xl shadow-border-xs">
|
||||||
</div>
|
<%= render "transactions/upcoming" %>
|
||||||
</div>
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ en:
|
|||||||
upcoming: Upcoming Recurring Transactions
|
upcoming: Upcoming Recurring Transactions
|
||||||
projected: Projected
|
projected: Projected
|
||||||
recurring: Recurring
|
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
|
day_of_month: Day %{day} of month
|
||||||
identify_patterns: Identify Patterns
|
identify_patterns: Identify Patterns
|
||||||
cleanup_stale: Clean Up Stale
|
cleanup_stale: Clean Up Stale
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ en:
|
|||||||
overview: Overview
|
overview: Overview
|
||||||
settings: Settings
|
settings: Settings
|
||||||
tags_label: Tags
|
tags_label: Tags
|
||||||
|
tab_transactions: Transactions
|
||||||
|
tab_upcoming: Upcoming
|
||||||
uncategorized: "(uncategorized)"
|
uncategorized: "(uncategorized)"
|
||||||
activity_labels:
|
activity_labels:
|
||||||
buy: Buy
|
buy: Buy
|
||||||
@@ -95,6 +97,11 @@ en:
|
|||||||
transaction: transaction
|
transaction: transaction
|
||||||
transactions: transactions
|
transactions: transactions
|
||||||
import: Import
|
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
|
toggle_recurring_section: Toggle upcoming recurring transactions
|
||||||
search:
|
search:
|
||||||
filters:
|
filters:
|
||||||
|
|||||||
Reference in New Issue
Block a user