mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 05:09:38 +02:00
Feat transactions search (#532)
* gem: Add ransack gem * feat: Implement transactions search
This commit is contained in:
parent
7f2633f9da
commit
cca779d3c4
9 changed files with 116 additions and 33 deletions
3
Gemfile
3
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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
29
app/javascript/controllers/transactions_search_controller.js
Normal file
29
app/javascript/controllers/transactions_search_controller.js
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -34,14 +34,13 @@
|
|||
</div>
|
||||
</div>
|
||||
<div id="transactions" class="bg-white rounded-xl border border-alpha-black-25 shadow-xs p-4 space-y-4">
|
||||
<div class="flex gap-2">
|
||||
<div class="grow cursor-not-allowed">
|
||||
<%= 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| %>
|
||||
<div class="flex gap-2 mb-4">
|
||||
<div class="grow">
|
||||
<div class="relative flex items-center bg-white border border-gray-200 rounded-lg">
|
||||
<%= 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") %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div>
|
||||
<button class="cursor-not-allowed border border-gray-200 block h-full rounded-lg flex items-center gap-2 px-4">
|
||||
|
@ -49,11 +48,23 @@
|
|||
<p class="text-sm font-medium text-gray-900">Filter</p>
|
||||
</button>
|
||||
</div>
|
||||
<%= 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 %>
|
||||
<div>
|
||||
<%= 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" } %>
|
||||
|
||||
<!-- Hidden fields for date range -->
|
||||
<%= form.hidden_field :date_gteq, value: '', "data-transactions-search-target": "dateGteq" %>
|
||||
<%= form.hidden_field :date_lteq, value: '', "data-transactions-search-target": "dateLteq" %>
|
||||
</div>
|
||||
<div class="bg-gray-25 rounded-xl px-5 py-3 text-xs font-medium text-gray-500 flex items-center gap-6">
|
||||
</div>
|
||||
<% end %>
|
||||
<%= turbo_frame_tag "transactions_list" do %>
|
||||
<% if @transactions.empty? %>
|
||||
<div class="flex flex-col items-center justify-center py-40">
|
||||
<p class="text-gray-500 mb-2">No transactions found</p>
|
||||
<p class="text-gray-400 max-w-xs text-center">Try adding a transaction, editing filters or refining your search</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="bg-gray-25 rounded-xl px-5 py-3 text-xs font-medium text-gray-500 flex items-center gap-6 mb-4">
|
||||
<div class="w-96">
|
||||
<p class="uppercase">transaction</p>
|
||||
</div>
|
||||
|
@ -62,16 +73,16 @@
|
|||
<p>amount</p>
|
||||
</div>
|
||||
</div>
|
||||
<% if @transactions.empty? %>
|
||||
<p class="text-gray-500 py-4">No transactions for this account yet.</p>
|
||||
<% else %>
|
||||
|
||||
<div class="space-y-6">
|
||||
<% @transactions.group_by { |transaction| transaction.date }.each do |date, grouped_transactions| %>
|
||||
<%= render partial: "transactions/transaction_group", locals: { date: date, transactions: grouped_transactions } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @pagy.pages > 1 %>
|
||||
<nav class="flex items-center justify-center px-4 sm:px-0">
|
||||
<nav class="flex items-center justify-center px-4 mt-4 sm:px-0">
|
||||
<%= render partial: "transactions/pagination", locals: { pagy: @pagy } %>
|
||||
</nav>
|
||||
<% end %>
|
||||
|
|
|
@ -216,6 +216,9 @@ module.exports = {
|
|||
fontSize: {
|
||||
"2xs": ".625rem",
|
||||
},
|
||||
spacing: {
|
||||
'40': '10rem',
|
||||
},
|
||||
keyframes: {
|
||||
"appear-then-fade": {
|
||||
"0%,100%": { opacity: 0 },
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue