1
0
Fork 0
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:
Ciocanel Razvan 2024-03-11 14:51:16 +02:00 committed by GitHub
parent 7f2633f9da
commit cca779d3c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 116 additions and 33 deletions

View file

@ -26,6 +26,9 @@ gem "turbo-rails"
# Background Jobs
gem "good_job"
# Search
gem "ransack"
# Other
gem "bcrypt", "~> 3.1.7"
gem "inline_svg"

View file

@ -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

View file

@ -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

View 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();
}
}

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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 %>

View file

@ -216,6 +216,9 @@ module.exports = {
fontSize: {
"2xs": ".625rem",
},
spacing: {
'40': '10rem',
},
keyframes: {
"appear-then-fade": {
"0%,100%": { opacity: 0 },