diff --git a/Gemfile b/Gemfile index 1adb7b690e3..48f6b8e050d 100644 --- a/Gemfile +++ b/Gemfile @@ -25,9 +25,6 @@ gem "turbo-rails" # Background Jobs gem "good_job" -# Search -gem "ransack", github: "maybe-finance/ransack", branch: "main" - # Error logging gem "stackprof" gem "sentry-ruby" diff --git a/Gemfile.lock b/Gemfile.lock index c845ab3bc0f..ac280841c99 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,16 +5,6 @@ GIT lucide-rails (0.2.0) railties (>= 4.1.0) -GIT - remote: https://github.com/maybe-finance/ransack.git - revision: dec20edc9ccccac77f5b4b8a1c1a9f20dc58fa04 - branch: main - specs: - ransack (4.1.1) - activerecord (>= 6.1.5) - activesupport (>= 6.1.5) - i18n - GIT remote: https://github.com/rails/rails.git revision: c1f1b14adce5cd373ed63611486eb7a7db73c78c @@ -494,7 +484,6 @@ DEPENDENCIES puma (>= 5.0) rails! rails-settings-cached - ransack! rubocop-rails-omakase ruby-lsp-rails selenium-webdriver diff --git a/app/controllers/transactions/rows_controller.rb b/app/controllers/transactions/rows_controller.rb new file mode 100644 index 00000000000..159e75a9c78 --- /dev/null +++ b/app/controllers/transactions/rows_controller.rb @@ -0,0 +1,22 @@ +class Transactions::RowsController < ApplicationController + before_action :set_transaction, only: %i[ show update ] + + def show + end + + def update + @transaction.update! transaction_params + + redirect_to transaction_row_path(@transaction) + end + + private + + def transaction_params + params.require(:transaction).permit(:category_id) + end + + def set_transaction + @transaction = Current.family.transactions.find(params[:id]) + end +end diff --git a/app/controllers/transactions_controller.rb b/app/controllers/transactions_controller.rb index 5208e1e5097..bc12c6772d5 100644 --- a/app/controllers/transactions_controller.rb +++ b/app/controllers/transactions_controller.rb @@ -4,57 +4,15 @@ class TransactionsController < ApplicationController before_action :set_transaction, only: %i[ show edit update destroy ] def index - search_params = session[ransack_session_key] || params[:q] - @q = Current.family.transactions.ransack(search_params) - result = @q.result.order(date: :desc) - @pagy, @transactions = pagy(result, items: 10) + @q = search_params + result = Current.family.transactions.search(@q).ordered + @pagy, @transactions = pagy(result, items: 50) + @totals = { count: result.count, income: result.inflows.sum(&:amount_money).abs, expense: result.outflows.sum(&:amount_money).abs } - @filter_list, valid_params = Transaction.build_filter_list(search_params, Current.family) - session[ransack_session_key] = valid_params - - respond_to do |format| - format.html - format.turbo_stream - end - end - - def search - if params[:clear] - session.delete(ransack_session_key) - elsif params[:remove_param] - current_params = session[ransack_session_key] || {} - if params[:remove_param] == "date_range" - updated_params = current_params.except("date_gteq", "date_lteq") - elsif params[:remove_param_value] - key_to_remove = params[:remove_param] - value_to_remove = params[:remove_param_value] - updated_params = current_params.deep_dup - updated_params[key_to_remove] = updated_params[key_to_remove] - [ value_to_remove ] - else - updated_params = current_params.except(params[:remove_param]) - end - session[ransack_session_key] = updated_params - elsif params[:q] - session[ransack_session_key] = params[:q] - end - - index - - respond_to do |format| - format.html { render :index } - format.turbo_stream do - render turbo_stream: [ - turbo_stream.replace("transactions_summary", partial: "transactions/summary", locals: { totals: @totals }), - turbo_stream.replace("transactions_search_form", partial: "transactions/search_form", locals: { q: @q }), - turbo_stream.replace("transactions_filters", partial: "transactions/filters", locals: { filters: @filter_list }), - turbo_stream.replace("transactions_list", partial: "transactions/list", locals: { transactions: @transactions, pagy: @pagy }) - ] - end - end end def show @@ -76,80 +34,28 @@ def create .find(params[:transaction][:account_id]) .transactions.build(transaction_params.merge(amount: amount)) - respond_to do |format| - if @transaction.save - @transaction.account.sync_later(@transaction.date) - format.html { redirect_to transactions_url, notice: t(".success") } - else - format.html { render :new, status: :unprocessable_entity } - end - end + @transaction.save! + @transaction.sync_account_later + redirect_to transactions_url, notice: t(".success") end def update - respond_to do |format| - sync_start_date = if transaction_params[:date] - [ @transaction.date, Date.parse(transaction_params[:date]) ].compact.min - else - @transaction.date - end - - if params[:transaction][:tag_id].present? - tag = Current.family.tags.find(params[:transaction][:tag_id]) - @transaction.tags << tag unless @transaction.tags.include?(tag) - end + @transaction.update! transaction_params + @transaction.sync_account_later - if params[:transaction][:remove_tag_id].present? - @transaction.tags.delete(params[:transaction][:remove_tag_id]) - end - - if @transaction.update(transaction_params) - @transaction.account.sync_later(sync_start_date) - - format.html { redirect_to transaction_url(@transaction), notice: t(".success") } - format.turbo_stream do - render turbo_stream: [ - turbo_stream.append("notification-tray", partial: "shared/notification", locals: { type: "success", content: { body: t(".success") } }), - turbo_stream.replace("transaction_#{@transaction.id}", partial: "transactions/transaction", locals: { transaction: @transaction }) - ] - end - else - format.html { render :edit, status: :unprocessable_entity } - end - end + redirect_to transaction_url(@transaction), notice: t(".success") end def destroy - @account = @transaction.account - sync_start_date = @account.transactions.where("date < ?", @transaction.date).order(date: :desc).first&.date @transaction.destroy! - @account.sync_later(sync_start_date) - - respond_to do |format| - format.html { redirect_to transactions_url, notice: t(".success") } - end + @transaction.sync_account_later + redirect_to transactions_url, notice: t(".success") end private - def delete_search_param(params, key, value: nil) - if value - params[key]&.delete(value) - params.delete(key) if params[key].empty? # Remove key if it's empty after deleting value - else - params.delete(key) - end - - params - end - - def ransack_session_key - :ransack_transactions_q - end - - # Use callbacks to share common setup or constraints between actions. def set_transaction - @transaction = Transaction.find(params[:id]) + @transaction = Current.family.transactions.find(params[:id]) end def amount @@ -164,7 +70,11 @@ def nature params[:transaction][:nature].to_s.inquiry end + def search_params + params.fetch(:q, {}).permit(:start_date, :end_date, :search, accounts: [], account_ids: [], categories: [], merchants: []) + end + def transaction_params - params.require(:transaction).permit(:name, :date, :amount, :currency, :notes, :excluded, :category_id, :merchant_id, :tag_id, :remove_tag_id).except(:tag_id, :remove_tag_id) + params.require(:transaction).permit(:name, :date, :amount, :currency, :notes, :excluded, :category_id, :merchant_id, tag_ids: [], taggings_attributes: [ :id, :tag_id, :_destroy ]) end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 288c3a78fb1..67082b18f71 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -20,23 +20,37 @@ def notification(text, **options, &block) render partial: "shared/notification", locals: { type: options[:type], content: { body: content } } end - # Wrap view with <%= modal do %> ... <% end %> to have it open in a modal - # Make sure to add data-turbo-frame="modal" to the link/button that opens the modal + ## + # Helper to open a centered and overlayed modal with custom contents + # + # @example Basic usage + # <%= modal classes: "custom-class" do %> + #
Content here
+ # <% end %> + # def modal(options = {}, &block) content = capture &block render partial: "shared/modal", locals: { content:, classes: options[:classes] } end + ## + # Helper to open a drawer on the right side of the screen with custom contents + # + # @example Basic usage + # <%= drawer do %> + #
Content here
+ # <% end %> + # + def drawer(&block) + content = capture &block + render partial: "shared/drawer", locals: { content: content } + end + def account_groups(period: nil) assets, liabilities = Current.family.accounts.by_group(currency: Current.family.currency, period: period || Period.last_30_days).values_at(:assets, :liabilities) [ assets.children, liabilities.children ].flatten end - def sidebar_modal(&block) - content = capture &block - render partial: "shared/sidebar_modal", locals: { content: content } - end - def sidebar_link_to(name, path, options = {}) is_current = current_page?(path) || (request.path.start_with?(path) && path != "/") diff --git a/app/helpers/transactions/searches_helper.rb b/app/helpers/transactions/searches_helper.rb new file mode 100644 index 00000000000..09bd882aafa --- /dev/null +++ b/app/helpers/transactions/searches_helper.rb @@ -0,0 +1,37 @@ +module Transactions::SearchesHelper + def transaction_search_filters + [ + { key: "account_filter", name: "Account", icon: "layers" }, + { key: "date_filter", name: "Date", icon: "calendar" }, + { key: "type_filter", name: "Type", icon: "shapes" }, + { key: "amount_filter", name: "Amount", icon: "hash" }, + { key: "category_filter", name: "Category", icon: "tag" }, + { key: "merchant_filter", name: "Merchant", icon: "store" } + ] + end + + def get_transaction_search_filter_partial_path(filter) + "transactions/searches/filters/#{filter[:key]}" + end + + def get_default_transaction_search_filter + transaction_search_filters[0] + end + + def transactions_path_without_param(param_key, param_value) + updated_params = request.query_parameters.deep_dup + + q_params = updated_params[:q] || {} + + current_value = q_params[param_key] + if current_value.is_a?(Array) + q_params[param_key] = current_value - [ param_value ] + else + q_params.delete(param_key) + end + + updated_params[:q] = q_params + + transactions_path(updated_params) + end +end diff --git a/app/helpers/transactions_helper.rb b/app/helpers/transactions_helper.rb index 7f270a1f8da..f5f4e9c4e22 100644 --- a/app/helpers/transactions_helper.rb +++ b/app/helpers/transactions_helper.rb @@ -1,24 +1,20 @@ module TransactionsHelper - def transaction_filters - [ - { name: "Account", partial: "account_filter", icon: "layers" }, - { name: "Date", partial: "date_filter", icon: "calendar" }, - { name: "Type", partial: "type_filter", icon: "shapes" }, - { name: "Amount", partial: "amount_filter", icon: "hash" }, - { name: "Category", partial: "category_filter", icon: "tag" }, - { name: "Merchant", partial: "merchant_filter", icon: "store" } - ] - end + def transactions_group(date, transactions, transaction_partial_path = "transactions/transaction") + header_left = content_tag :span do + "#{date.strftime('%b %d, %Y')} ยท #{transactions.size}".html_safe + end - def transaction_filter_id(filter) - "txn-#{filter[:name].downcase}-filter" - end + header_right = content_tag :span do + format_money(-transactions.sum(&:amount_money)) + end - def transaction_filter_by_name(name) - transaction_filters.find { |filter| filter[:name] == name } - end + header = header_left.concat(header_right) + + content = render partial: transaction_partial_path, collection: transactions - def full_width_transaction_row?(route) - route != "/" + render partial: "shared/list_group", locals: { + header: header, + content: content + } end end diff --git a/app/models/account.rb b/app/models/account.rb index 2550acef542..c77f498f53e 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -22,10 +22,6 @@ class Account < ApplicationRecord delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy - def self.ransackable_attributes(auth_object = nil) - %w[name id] - end - def balance_on(date) balances.where("date <= ?", date).order(date: :desc).first&.balance end diff --git a/app/models/transaction.rb b/app/models/transaction.rb index 75e4f63ce61..4ffdc137bca 100644 --- a/app/models/transaction.rb +++ b/app/models/transaction.rb @@ -1,20 +1,28 @@ class Transaction < ApplicationRecord include Monetizable + monetize :amount + belongs_to :account belongs_to :category, optional: true belongs_to :merchant, optional: true - has_many :taggings, as: :taggable, dependent: :destroy has_many :tags, through: :taggings + accepts_nested_attributes_for :taggings, allow_destroy: true validates :name, :date, :amount, :account, presence: true - monetize :amount - + scope :ordered, -> { order(date: :desc) } + scope :active, -> { where(excluded: false) } scope :inflows, -> { where("amount <= 0") } scope :outflows, -> { where("amount > 0") } - scope :active, -> { where(excluded: false) } + scope :by_name, ->(name) { where("transactions.name ILIKE ?", "%#{name}%") } + scope :with_categories, ->(categories) { joins(:category).where(transaction_categories: { name: categories }) } + scope :with_accounts, ->(accounts) { joins(:account).where(accounts: { name: accounts }) } + scope :with_account_ids, ->(account_ids) { joins(:account).where(accounts: { id: account_ids }) } + scope :with_merchants, ->(merchants) { joins(:merchant).where(transaction_merchants: { name: merchants }) } + scope :on_or_after_date, ->(date) { where("transactions.date >= ?", date) } + scope :on_or_before_date, ->(date) { where("transactions.date <= ?", date) } scope :with_converted_amount, ->(currency = Current.family.currency) { # Join with exchange rates to convert the amount to the given currency # If no rate is available, exclude the transaction from the results @@ -26,92 +34,74 @@ class Transaction < ApplicationRecord .where("er.rate IS NOT NULL OR transactions.currency = ?", currency) } - def self.daily_totals(transactions, period: Period.last_30_days, currency: Current.family.currency) - # Sum spending and income for each day in the period with the given currency - select( - "gs.date", - "COALESCE(SUM(converted_amount) FILTER (WHERE converted_amount > 0), 0) AS spending", - "COALESCE(SUM(-converted_amount) FILTER (WHERE converted_amount < 0), 0) AS income" - ) - .from(transactions.with_converted_amount(currency), :t) - .joins(sanitize_sql([ "RIGHT JOIN generate_series(?, ?, interval '1 day') AS gs(date) ON t.date = gs.date", period.date_range.first, period.date_range.last ])) - .group("gs.date") + def inflow? + amount <= 0 end - def self.daily_rolling_totals(transactions, period: Period.last_30_days, currency: Current.family.currency) - # Extend the period to include the rolling window - period_with_rolling = period.extend_backward(period.date_range.count.days) - - # Aggregate the rolling sum of spending and income based on daily totals - rolling_totals = from(daily_totals(transactions, period: period_with_rolling, currency: currency)) - .select( - "*", - sanitize_sql_array([ "SUM(spending) OVER (ORDER BY date RANGE BETWEEN INTERVAL ? PRECEDING AND CURRENT ROW) as rolling_spend", "#{period.date_range.count} days" ]), - sanitize_sql_array([ "SUM(income) OVER (ORDER BY date RANGE BETWEEN INTERVAL ? PRECEDING AND CURRENT ROW) as rolling_income", "#{period.date_range.count} days" ]) - ) - .order("date") - - # Trim the results to the original period - select("*").from(rolling_totals).where("date >= ?", period.date_range.first) + def outflow? + amount > 0 end - def self.ransackable_attributes(auth_object = nil) - %w[name amount date] - end + def sync_account_later + if destroyed? + sync_start_date = previous_transaction_date + else + sync_start_date = [ date_previously_was, date ].compact.min + end - def self.ransackable_associations(auth_object = nil) - %w[category merchant account] + account.sync_later(sync_start_date) end - def self.build_filter_list(params, family) - filters = [] - valid_params = {} + class << self + def daily_totals(transactions, period: Period.last_30_days, currency: Current.family.currency) + # Sum spending and income for each day in the period with the given currency + select( + "gs.date", + "COALESCE(SUM(converted_amount) FILTER (WHERE converted_amount > 0), 0) AS spending", + "COALESCE(SUM(-converted_amount) FILTER (WHERE converted_amount < 0), 0) AS income" + ) + .from(transactions.with_converted_amount(currency), :t) + .joins(sanitize_sql([ "RIGHT JOIN generate_series(?, ?, interval '1 day') AS gs(date) ON t.date = gs.date", period.date_range.first, period.date_range.last ])) + .group("gs.date") + end - date_filters = { gteq: nil, lteq: nil } + def daily_rolling_totals(transactions, period: Period.last_30_days, currency: Current.family.currency) + # Extend the period to include the rolling window + period_with_rolling = period.extend_backward(period.date_range.count.days) + + # Aggregate the rolling sum of spending and income based on daily totals + rolling_totals = from(daily_totals(transactions, period: period_with_rolling, currency: currency)) + .select( + "*", + sanitize_sql_array([ "SUM(spending) OVER (ORDER BY date RANGE BETWEEN INTERVAL ? PRECEDING AND CURRENT ROW) as rolling_spend", "#{period.date_range.count} days" ]), + sanitize_sql_array([ "SUM(income) OVER (ORDER BY date RANGE BETWEEN INTERVAL ? PRECEDING AND CURRENT ROW) as rolling_income", "#{period.date_range.count} days" ]) + ) + .order("date") + + # Trim the results to the original period + select("*").from(rolling_totals).where("date >= ?", period.date_range.first) + end - if params - params.each do |key, value| - next if value.blank? + def search(params) + query = all + query = query.by_name(params[:search]) if params[:search].present? + query = query.with_categories(params[:categories]) if params[:categories].present? + query = query.with_accounts(params[:accounts]) if params[:accounts].present? + query = query.with_account_ids(params[:account_ids]) if params[:account_ids].present? + query = query.with_merchants(params[:merchants]) if params[:merchants].present? + query = query.on_or_after_date(params[:start_date]) if params[:start_date].present? + query = query.on_or_before_date(params[:end_date]) if params[:end_date].present? + query + end + end - case key - when "account_id_in" - valid_accounts = value.select do |account_id| - account = family.accounts.find_by(id: account_id) - filters << { type: "account", value: account, original: { key: key, value: account_id } } if account.present? - account.present? - end - valid_params[key] = valid_accounts unless valid_accounts.empty? - when "category_id_in" - valid_categories = value.select do |category_id| - category = family.transaction_categories.find_by(id: category_id) - filters << { type: "category", value: category, original: { key: key, value: category_id } } if category.present? - category.present? - end - valid_params[key] = valid_categories unless valid_categories.empty? - when "merchant_id_in" - valid_merchants = value.select do |merchant_id| - merchant = family.transaction_merchants.find_by(id: merchant_id) - filters << { type: "merchant", value: merchant, original: { key: key, value: merchant_id } } if merchant.present? - merchant.present? - end - valid_params[key] = valid_merchants unless valid_merchants.empty? - when "category_name_or_merchant_name_or_account_name_or_name_cont" - filters << { type: "search", value: value, original: { key: key, value: nil } } - valid_params[key] = value - when "date_gteq" - date_filters[:gteq] = value - valid_params[key] = value - when "date_lteq" - date_filters[:lteq] = value - valid_params[key] = value - end - end + private - unless date_filters.values.compact.empty? - filters << { type: "date_range", value: date_filters, original: { key: "date_range", value: nil } } - end + def previous_transaction_date + self.account + .transactions + .where("date < ?", date) + .order(date: :desc) + .first&.date end - - [ filters, valid_params ] - end end diff --git a/app/models/transaction/category.rb b/app/models/transaction/category.rb index d884a3bdfb0..58955720f99 100644 --- a/app/models/transaction/category.rb +++ b/app/models/transaction/category.rb @@ -23,14 +23,6 @@ class Transaction::Category < ApplicationRecord { internal_category: "home_improvement", color: COLORS[7] } ] - def self.ransackable_attributes(auth_object = nil) - %w[name id] - end - - def self.ransackable_associations(auth_object = nil) - %w[] - end - def self.create_default_categories(family) if family.transaction_categories.size > 0 raise ArgumentError, "Family already has some categories" diff --git a/app/models/transaction/merchant.rb b/app/models/transaction/merchant.rb index e0395bb86e2..369ab916588 100644 --- a/app/models/transaction/merchant.rb +++ b/app/models/transaction/merchant.rb @@ -7,12 +7,4 @@ class Transaction::Merchant < ApplicationRecord scope :alphabetically, -> { order(:name) } COLORS = %w[#e99537 #4da568 #6471eb #db5a54 #df4e92 #c44fe9 #eb5429 #61c9ea #805dee #6ad28a] - - def self.ransackable_attributes(auth_object = nil) - %w[name id] - end - - def self.ransackable_associations(auth_object = nil) - %w[] - end end diff --git a/app/views/accounts/_transactions.html.erb b/app/views/accounts/_transactions.html.erb index ab72aa206f1..c60eef767d7 100644 --- a/app/views/accounts/_transactions.html.erb +++ b/app/views/accounts/_transactions.html.erb @@ -7,11 +7,14 @@ New transaction <% end %> + <% if transactions.empty? %>

No transactions for this account yet.

<% else %>
- <%= render partial: "transactions/transaction_group", collection: transactions.group_by(&:date), as: :transaction_group %> + <% transactions.group_by(&:date).each do |date, transactions| %> + <%= transactions_group(date, transactions) %> + <% end %>
<% end %> diff --git a/app/views/imports/confirm.html.erb b/app/views/imports/confirm.html.erb index 56ac1975428..32694dff232 100644 --- a/app/views/imports/confirm.html.erb +++ b/app/views/imports/confirm.html.erb @@ -9,7 +9,9 @@
- <%= render partial: "imports/transactions/transaction_group", collection: @import.dry_run.group_by(&:date) %> + <% @import.dry_run.group_by(&:date).each do |date, draft_transactions| %> + <%= transactions_group(date, draft_transactions, "imports/transactions/transaction") %> + <% end %>
<%= button_to "Import " + @import.csv.table.size.to_s + " transactions", confirm_import_path(@import), method: :patch, class: "px-4 py-2 block w-60 text-center mx-auto rounded-lg bg-gray-900 text-white text-sm font-medium", data: { turbo: false } %> diff --git a/app/views/imports/transactions/_transaction.html.erb b/app/views/imports/transactions/_transaction.html.erb index 9fbcdef3b01..a467c3c30e4 100644 --- a/app/views/imports/transactions/_transaction.html.erb +++ b/app/views/imports/transactions/_transaction.html.erb @@ -1,18 +1,20 @@ <%# locals: (transaction:) %> -
- <%= render partial: "transactions/transaction_name", locals: { name: transaction.name } %> +
+
+ <%= render "transactions/name", transaction: transaction %> +
-
+
<%= render partial: "transactions/categories/badge", locals: { category: transaction.category } %>
-
+
<% transaction.tags.each do |tag| %> <%= render partial: "tags/badge", locals: { tag: tag } %> <% end %>
-
- <%= content_tag :p, format_money(Money.new(-transaction.amount, @import.account.currency)), class: ["whitespace-nowrap", BigDecimal(transaction.amount).negative? ? "text-green-600" : "text-red-600"] %> +
+ <%= render "transactions/amount", transaction: transaction %>
diff --git a/app/views/imports/transactions/_transaction_group.html.erb b/app/views/imports/transactions/_transaction_group.html.erb deleted file mode 100644 index fae987924ba..00000000000 --- a/app/views/imports/transactions/_transaction_group.html.erb +++ /dev/null @@ -1,13 +0,0 @@ -<%# locals: (transaction_group:) %> -<% date = transaction_group[0] %> -<% transactions = transaction_group[1] %> - -
-
-

<%= date.strftime("%b %d, %Y") %> · <%= transactions.size %>

- <%= format_money Money.new(-transactions.sum { |t| t.amount }, @import.account.currency) %> -
-
- <%= render partial: "imports/transactions/transaction", collection: transactions %> -
-
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index ddef53e26e1..dbf9398f575 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,5 +1,5 @@ - + <%= content_for(:title) || "Maybe" %> @@ -13,7 +13,8 @@ <%= hotwire_livereload_tags if Rails.env.development? %> <%= turbo_refreshes_with method: :morph, scroll: :preserve %> - + @@ -30,10 +31,13 @@ <%= content_for?(:content) ? yield(:content) : yield %> <%= turbo_frame_tag "modal" %> + <%= turbo_frame_tag "drawer" %> + <%= render "shared/confirm_modal" %> <% if self_hosted? %> <%= render "shared/app_version" %> <% end %> + diff --git a/app/views/pages/dashboard.html.erb b/app/views/pages/dashboard.html.erb index 8c527050d95..a240cc97aa3 100644 --- a/app/views/pages/dashboard.html.erb +++ b/app/views/pages/dashboard.html.erb @@ -161,9 +161,12 @@

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

<% else %> -
- <%= render partial: "transactions/transaction_group", collection: @transactions.group_by(&:date), as: :transaction_group %> -

<%= link_to t(".view_all"), transactions_path %>

+
+ <% @transactions.group_by(&:date).each do |date, transactions| %> + <%= transactions_group(date, transactions, "pages/dashboard/transactions/transaction") %> + <% end %> + +

<%= link_to t(".view_all"), transactions_path %>

<% end %>
diff --git a/app/views/pages/dashboard/transactions/_transaction.html.erb b/app/views/pages/dashboard/transactions/_transaction.html.erb new file mode 100644 index 00000000000..b8dbc57b3dc --- /dev/null +++ b/app/views/pages/dashboard/transactions/_transaction.html.erb @@ -0,0 +1,9 @@ +
+
+ <%= render "transactions/name", transaction: transaction %> +
+ +
+ <%= render "transactions/amount", transaction: transaction %> +
+
diff --git a/app/views/shared/_sidebar_modal.html.erb b/app/views/shared/_drawer.html.erb similarity index 94% rename from app/views/shared/_sidebar_modal.html.erb rename to app/views/shared/_drawer.html.erb index c2d7a848a4b..321667c8236 100644 --- a/app/views/shared/_sidebar_modal.html.erb +++ b/app/views/shared/_drawer.html.erb @@ -1,4 +1,4 @@ -<%= turbo_frame_tag "modal" do %> +<%= turbo_frame_tag "drawer" do %>
diff --git a/app/views/shared/_list_group.html.erb b/app/views/shared/_list_group.html.erb new file mode 100644 index 00000000000..526bb503d7d --- /dev/null +++ b/app/views/shared/_list_group.html.erb @@ -0,0 +1,9 @@ +<%# locals: (header:, content:) %> +
+
+ <%= header %> +
+
+ <%= content %> +
+
diff --git a/app/views/transactions/_amount.html.erb b/app/views/transactions/_amount.html.erb new file mode 100644 index 00000000000..1ebf81ff726 --- /dev/null +++ b/app/views/transactions/_amount.html.erb @@ -0,0 +1,3 @@ +<%= content_tag :p, + format_money(-transaction.amount_money), + class: ["text-green-600": transaction.inflow?] %> diff --git a/app/views/transactions/_empty.html.erb b/app/views/transactions/_empty.html.erb new file mode 100644 index 00000000000..9cb5e3fa7a3 --- /dev/null +++ b/app/views/transactions/_empty.html.erb @@ -0,0 +1,4 @@ +
+

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

+

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

+
diff --git a/app/views/transactions/_filter.html.erb b/app/views/transactions/_filter.html.erb deleted file mode 100644 index f40246111ea..00000000000 --- a/app/views/transactions/_filter.html.erb +++ /dev/null @@ -1,48 +0,0 @@ -<%# locals: (filter:) %> -
- <% case filter[:type] %> - <% when "account" %> -
-
<%= filter[:value].name[0].upcase %>
-

<%= filter[:value].name %>

-
- <% when "category" %> -
-
-

<%= filter[:value].name %>

-
- <% when "merchant" %> -
-
-

<%= filter[:value].name %>

-
- <% when "search" %> -
- <%= lucide_icon "text", class: "w-5 h-5 text-gray-500" %> -

<%= "\"#{filter[:value]}\"".truncate(20) %>

-
- <% when "date_range" %> -
- <%= lucide_icon "calendar", class: "w-5 h-5 text-gray-500" %> -

- <% if filter[:value][:gteq] && filter[:value][:lteq] %> - <%= filter[:value][:gteq] %> → <%= filter[:value][:lteq] %> - <% elsif filter[:value][:gteq] %> - on or after <%= filter[:value][:gteq] %> - <% elsif filter[:value][:lteq] %> - on or before <%= filter[:value][:lteq] %> - <% end %> -

-
- <% end %> - <%= form_with url: search_transactions_path, html: { class: "flex items-center" } do |form| %> - <%= form.hidden_field :remove_param, value: filter[:original][:key] %> - <% if filter[:original][:value] %> - <%= form.hidden_field :remove_param_value, value: filter[:original][:value] %> - <% else %> - <% end %> - <%= form.button type: "submit", class: "hover:text-gray-900" do %> - <%= lucide_icon "x", class: "w-4 h-4 text-gray-500" %> - <% end %> - <% end %> -
diff --git a/app/views/transactions/_filters.html.erb b/app/views/transactions/_filters.html.erb deleted file mode 100644 index 5349f2b05b5..00000000000 --- a/app/views/transactions/_filters.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -<%# locals: (filters:) %> -
- <%= turbo_frame_tag "transactions_filters" do %> -
- <% filters.each do |filter| %> - <%= render partial: "transactions/filter", locals: { filter: filter } %> - <% end %> -
- <% end %> -
diff --git a/app/views/transactions/_header.html.erb b/app/views/transactions/_header.html.erb new file mode 100644 index 00000000000..f34690b895a --- /dev/null +++ b/app/views/transactions/_header.html.erb @@ -0,0 +1,33 @@ +
+

Transactions

+
+
+ <%= contextual_menu do %> +
+ <%= link_to transaction_categories_path, + class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg font-normal" do %> + <%= lucide_icon "tags", class: "w-5 h-5 text-gray-500" %> + <%= t(".edit_categories") %> + <% end %> + + <%= link_to imports_path, + class: "block w-full py-2 px-3 space-x-2 text-gray-900 hover:bg-gray-50 flex items-center rounded-lg font-normal" do %> + <%= lucide_icon "hard-drive-upload", class: "w-5 h-5 text-gray-500" %> + <%= t(".edit_imports") %> + <% end %> +
+ + <% end %> + + <%= link_to new_import_path(enable_type_selector: true), class: "rounded-lg bg-gray-50 border border-gray-200 flex items-center gap-1 justify-center px-3 py-2", data: { turbo_frame: "modal" } do %> + <%= lucide_icon("download", class: "text-gray-500 w-4 h-4") %> +

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

+ <% end %> + + <%= link_to new_transaction_path, class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2", data: { turbo_frame: :modal } do %> + <%= lucide_icon("plus", class: "w-5 h-5") %> +

New transaction

+ <% end %> +
+
+
diff --git a/app/views/transactions/_list.html.erb b/app/views/transactions/_list.html.erb deleted file mode 100644 index 775f677b898..00000000000 --- a/app/views/transactions/_list.html.erb +++ /dev/null @@ -1,32 +0,0 @@ -<%# locals: (transactions:, pagy:) %> -
- <%= turbo_frame_tag "transactions_list" do %> - <% if transactions.empty? %> -
-

No transactions found

-

Try adding a transaction, editing filters or refining your search

-
- <% else %> -
-
-

transaction

-
-
-

category

-
-
-

account

-

amount

-
-
-
- <%= render partial: "transactions/transaction_group", collection: transactions.group_by(&:date), as: :transaction_group %> -
- <% end %> - <% if pagy.pages > 1 %> - - <% end %> - <% end %> -
diff --git a/app/views/transactions/_name.html.erb b/app/views/transactions/_name.html.erb new file mode 100644 index 00000000000..6f27f324ec3 --- /dev/null +++ b/app/views/transactions/_name.html.erb @@ -0,0 +1,16 @@ +<%= content_tag :div, class: ["flex items-center gap-2"] do %> +
+ <%= transaction.name[0].upcase %> +
+ +
+ <% if transaction.new_record? %> + <%= content_tag :p, transaction.name %> + <% else %> + <%= link_to transaction.name, + transaction_path(transaction), + data: { turbo_frame: "drawer" }, + class: "hover:underline hover:text-gray-800" %> + <% end %> +
+<% end %> diff --git a/app/views/transactions/_pagination.html.erb b/app/views/transactions/_pagination.html.erb index a64b3bf5256..2ad3149ab56 100644 --- a/app/views/transactions/_pagination.html.erb +++ b/app/views/transactions/_pagination.html.erb @@ -1,24 +1,7 @@ - -
- <% if pagy.prev %> - <%= link_to "Previous", pagy_url_for(pagy, pagy.prev), class: "relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50" %> - <% else %> -
Previous
- <% end %> - - <% if pagy.next %> - <%= link_to "Next", pagy_url_for(pagy, pagy.next), class: "relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50" %> - <% else %> -
Next
- <% end %> -
- - - -