From cca779d3c45a10908159cf663c69dab9772ab414 Mon Sep 17 00:00:00 2001 From: Ciocanel Razvan Date: Mon, 11 Mar 2024 14:51:16 +0200 Subject: [PATCH] Feat transactions search (#532) * gem: Add ransack gem * feat: Implement transactions search --- Gemfile | 3 + Gemfile.lock | 5 ++ app/controllers/transactions_controller.rb | 14 +++- .../transactions_search_controller.js | 29 +++++++ app/models/account.rb | 4 + app/models/transaction.rb | 8 ++ app/models/transaction/category.rb | 8 ++ app/views/transactions/index.html.erb | 75 +++++++++++-------- config/tailwind.config.js | 3 + 9 files changed, 116 insertions(+), 33 deletions(-) create mode 100644 app/javascript/controllers/transactions_search_controller.js diff --git a/Gemfile b/Gemfile index 0192f367..f7bf9115 100644 --- a/Gemfile +++ b/Gemfile @@ -26,6 +26,9 @@ gem "turbo-rails" # Background Jobs gem "good_job" +# Search +gem "ransack" + # Other gem "bcrypt", "~> 3.1.7" gem "inline_svg" diff --git a/Gemfile.lock b/Gemfile.lock index 411ab898..68463645 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -278,6 +278,10 @@ GEM railties (>= 6.0.0, < 8) rainbow (3.1.1) rake (13.1.0) + ransack (4.1.1) + activerecord (>= 6.1.5) + activesupport (>= 6.1.5) + i18n rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) @@ -409,6 +413,7 @@ DEPENDENCIES propshaft puma (>= 5.0) rails! + ransack redis (>= 4.0.1) rubocop-rails-omakase ruby-lsp-rails diff --git a/app/controllers/transactions_controller.rb b/app/controllers/transactions_controller.rb index fb5d3e93..396694eb 100644 --- a/app/controllers/transactions_controller.rb +++ b/app/controllers/transactions_controller.rb @@ -3,7 +3,19 @@ class TransactionsController < ApplicationController before_action :set_transaction, only: %i[ show edit update destroy ] def index - @pagy, @transactions = pagy(Current.family.transactions.order(date: :desc), items: 50) + search_params = params[:q] || {} + period = Period.find_by_name(search_params[:date]) + if period&.date_range + search_params.merge!({ date_gteq: period.date_range.begin, date_lteq: period.date_range.end }) + end + + @q = Current.family.transactions.ransack(search_params) + @pagy, @transactions = pagy(@q.result.order(date: :desc), items: 50) + + respond_to do |format| + format.html # For full page reloads + format.turbo_stream # For Turbo Frame requests + end end def show diff --git a/app/javascript/controllers/transactions_search_controller.js b/app/javascript/controllers/transactions_search_controller.js new file mode 100644 index 00000000..8f47ae53 --- /dev/null +++ b/app/javascript/controllers/transactions_search_controller.js @@ -0,0 +1,29 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["search", "date"] + + connect() { + this.timeout = null + } + + search() { + clearTimeout(this.timeout); + this.timeout = setTimeout(() => { + this.submitForm(); + }, 300); // Debounce time in milliseconds + } + + submitForm() { + const formData = new FormData(this.searchTarget.form); + const searchParams = new URLSearchParams(formData).toString(); + const newUrl = `${window.location.pathname}?${searchParams}`; + + history.pushState({}, '', newUrl); + this.searchTarget.form.requestSubmit(); + } + + afterSubmit() { + this.searchTarget.focus(); + } +} diff --git a/app/models/account.rb b/app/models/account.rb index 0a280f77..ae1655c7 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -13,6 +13,10 @@ class Account < ApplicationRecord before_create :check_currency + def self.ransackable_attributes(auth_object = nil) + %w[name] + end + def trend(period = Period.all) first = balances.in_period(period).order(:date).first last = balances.in_period(period).order(date: :desc).first diff --git a/app/models/transaction.rb b/app/models/transaction.rb index 4ffa367d..b3ecf6b4 100644 --- a/app/models/transaction.rb +++ b/app/models/transaction.rb @@ -4,6 +4,14 @@ class Transaction < ApplicationRecord after_commit :sync_account + def self.ransackable_attributes(auth_object = nil) + %w[name amount date] + end + + def self.ransackable_associations(auth_object = nil) + %w[category account] + end + private def sync_account diff --git a/app/models/transaction/category.rb b/app/models/transaction/category.rb index 270accd2..5c7b370d 100644 --- a/app/models/transaction/category.rb +++ b/app/models/transaction/category.rb @@ -15,6 +15,14 @@ class Transaction::Category < ApplicationRecord { internal_category: "home_improvement", color: "#fdcce5" } ] + def self.ransackable_attributes(auth_object = nil) + %w[name] + 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/views/transactions/index.html.erb b/app/views/transactions/index.html.erb index 2ecaf472..aedcb523 100644 --- a/app/views/transactions/index.html.erb +++ b/app/views/transactions/index.html.erb @@ -34,44 +34,55 @@
-
-
- <%= form_with url: transactions_path, method: :get, local: true, html: { role: 'search' } do |form| %> + <%= search_form_for @q, url: transactions_path, method: :get, remote: true, html: { class: "search-form", "data-controller": "transactions-search", "data-action": "keyup->transactions-search#search change->transactions-search#search", "data-turbo-frame": "transactions_list" } do |form| %> +
+
- <%= form.text_field :search, placeholder: "Search transaction by merchant, category or amount", class: "placeholder:text-sm placeholder:text-gray-500 relative pl-10 w-full border-none rounded-lg cursor-not-allowed", 'data-action': "input->search#perform", disabled: true %> + <%= form.search_field :category_name_or_account_name_or_name_cont, placeholder: "Search transaction by name, category or amount", class: "placeholder:text-sm placeholder:text-gray-500 relative pl-10 w-full border-none rounded-lg", "data-transactions-search-target": "search", 'data-action': "input->transactions-search#search" %> <%= lucide_icon("search", class: "w-5 h-5 text-gray-500 ml-2 absolute inset-0 transform top-1/2 -translate-y-1/2") %>
- <% end %> +
+
+ +
+
+ <%= form.select :date, options_for_select([['All', 'all'], ['7D', 'last_7_days'], ['1M', 'last_30_days'], ["1Y", "last_365_days"]], selected: params.dig(:q, :date)), {}, { class: "block h-full w-full border border-gray-200 rounded-lg text-sm py-2 pr-8 pl-2", "data-transactions-search-target": "date" } %> + + + <%= form.hidden_field :date_gteq, value: '', "data-transactions-search-target": "dateGteq" %> + <%= form.hidden_field :date_lteq, value: '', "data-transactions-search-target": "dateLteq" %> +
-
- -
- <%= form_with url: "#", method: :get, class: "flex items-center gap-4", html: { class: "" } do |f| %> - <%= f.select :period, options_for_select([['7D', 'last_7_days'], ['1M', 'last_30_days'], ["1Y", "last_365_days"], ['All', 'all']], selected: params[:period]), {}, { class: "block h-full w-full border border-gray-200 rounded-lg text-sm py-2 pr-8 pl-2 cursor-not-allowed", onchange: "this.form.submit();", disabled: true } %> + <% end %> + <%= turbo_frame_tag "transactions_list" do %> + <% if @transactions.empty? %> +
+

No transactions found

+

Try adding a transaction, editing filters or refining your search

+
+ <% else %> +
+
+

transaction

+
+
+

account

+

amount

+
+
+ +
+ <% @transactions.group_by { |transaction| transaction.date }.each do |date, grouped_transactions| %> + <%= render partial: "transactions/transaction_group", locals: { date: date, transactions: grouped_transactions } %> + <% end %> +
<% end %> -
-
-
-

transaction

-
-
-

account

-

amount

-
-
- <% if @transactions.empty? %> -

No transactions for this account yet.

- <% else %> -
- <% @transactions.group_by { |transaction| transaction.date }.each do |date, grouped_transactions| %> - <%= render partial: "transactions/transaction_group", locals: { date: date, transactions: grouped_transactions } %> - <% end %> -
+ <% if @pagy.pages > 1 %> -