mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 13:19:39 +02:00
Implement transaction filtering UI (#578)
* Rough sketch of implementation * Consolidate auto submit controller * Store ransack params in session * Improve how summary is calculated for txns * Implement filters UI
This commit is contained in:
parent
7ae25dd6df
commit
f0c2d4ead0
20 changed files with 296 additions and 146 deletions
|
@ -3,18 +3,46 @@ class TransactionsController < ApplicationController
|
|||
before_action :set_transaction, only: %i[ show edit update destroy ]
|
||||
|
||||
def index
|
||||
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
|
||||
|
||||
search_params = session[ransack_session_key] || params[:q]
|
||||
@q = Current.family.transactions.ransack(search_params)
|
||||
@pagy, @transactions = pagy(@q.result.order(date: :desc), items: 50)
|
||||
result = @q.result.order(date: :desc)
|
||||
@pagy, @transactions = pagy(result, items: 10)
|
||||
@totals = {
|
||||
count: result.count,
|
||||
income: result.inflows.sum(&:amount_money).abs,
|
||||
expense: result.outflows.sum(&:amount_money).abs
|
||||
}
|
||||
@filter_list = Transaction.build_filter_list(search_params, Current.family)
|
||||
|
||||
respond_to do |format|
|
||||
format.html # For full page reloads
|
||||
format.turbo_stream # For Turbo Frame requests
|
||||
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] || {}
|
||||
updated_params = delete_search_param(current_params, params[:remove_param], value: params[:remove_param_value])
|
||||
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
|
||||
|
||||
|
@ -67,6 +95,21 @@ class TransactionsController < ApplicationController
|
|||
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])
|
||||
|
|
|
@ -1,31 +1,26 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
import { Controller } from "@hotwired/stimulus";
|
||||
|
||||
export default class extends Controller {
|
||||
|
||||
get cssInputSelector() {
|
||||
return 'input:not(.no-auto-submit), textarea:not(.no-auto-submit)';
|
||||
}
|
||||
|
||||
get inputElements() {
|
||||
return this.element.querySelectorAll(this.cssInputSelector);
|
||||
}
|
||||
|
||||
get selectElements() {
|
||||
return this.element.querySelectorAll('select:not(.no-auto-submit)');
|
||||
}
|
||||
// By default, auto-submit is "opt-in" to avoid unexpected behavior. Each `auto` target
|
||||
// will trigger a form submission when the input event is triggered.
|
||||
static targets = ["auto"];
|
||||
|
||||
connect() {
|
||||
[...this.inputElements, ...this.selectElements].forEach(el => el.addEventListener('change', this.handler));
|
||||
this.autoTargets.forEach((element) => {
|
||||
element.addEventListener("input", this.handleInput);
|
||||
});
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
[...this.inputElements, ...this.selectElements].forEach(el => el.removeEventListener('change', this.handler));
|
||||
}
|
||||
|
||||
handler = (e) => {
|
||||
console.log(e);
|
||||
this.element.requestSubmit();
|
||||
this.autoTargets.forEach((element) => {
|
||||
element.removeEventListener("input", this.handleInput);
|
||||
});
|
||||
}
|
||||
|
||||
handleInput = () => {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = setTimeout(() => {
|
||||
this.element.requestSubmit();
|
||||
}, 500);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
16
app/javascript/controllers/list_filter_controller.js
Normal file
16
app/javascript/controllers/list_filter_controller.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { Controller } from "@hotwired/stimulus";
|
||||
|
||||
// Basic functionality to filter a list based on a provided text attribute.
|
||||
export default class extends Controller {
|
||||
static targets = ["input", "list"];
|
||||
|
||||
filter() {
|
||||
const filterValue = this.inputTarget.value.toLowerCase();
|
||||
const items = this.listTarget.querySelectorAll(".filterable-item");
|
||||
|
||||
items.forEach((item) => {
|
||||
const text = item.getAttribute("data-filter-name").toLowerCase();
|
||||
item.style.display = text.includes(filterValue) ? "" : "none";
|
||||
});
|
||||
}
|
||||
}
|
|
@ -21,15 +21,17 @@ export default class extends Controller {
|
|||
|
||||
onTurboLoad = () => {
|
||||
this.updateClasses(this.defaultTabValue);
|
||||
}
|
||||
};
|
||||
|
||||
updateClasses = (selectedId) => {
|
||||
this.btnTargets.forEach((btn) => btn.classList.remove(this.activeClass));
|
||||
this.btnTargets.forEach((btn) =>
|
||||
btn.classList.remove(...this.activeClasses)
|
||||
);
|
||||
this.tabTargets.forEach((tab) => tab.classList.add("hidden"));
|
||||
|
||||
this.btnTargets.forEach((btn) => {
|
||||
if (btn.dataset.id === selectedId) {
|
||||
btn.classList.add(this.activeClass);
|
||||
btn.classList.add(...this.activeClasses);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -38,5 +40,5 @@ export default class extends Controller {
|
|||
tab.classList.remove("hidden");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
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();
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ class Account < ApplicationRecord
|
|||
delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
%w[name]
|
||||
%w[name id]
|
||||
end
|
||||
|
||||
def balance_on(date)
|
||||
|
|
|
@ -22,6 +22,27 @@ class Transaction < ApplicationRecord
|
|||
%w[category account]
|
||||
end
|
||||
|
||||
def self.build_filter_list(params, family)
|
||||
filters = []
|
||||
|
||||
if params
|
||||
params.each do |key, value|
|
||||
next if value.blank?
|
||||
|
||||
case key
|
||||
when "account_id_in"
|
||||
value.each do |account_id|
|
||||
filters << { type: "account", value: family.accounts.find(account_id), original: { key: key, value: account_id } }
|
||||
end
|
||||
when "category_name_or_account_name_or_name_cont"
|
||||
filters << { type: "search", value: value, original: { key: key, value: nil } }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
filters
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sync_account
|
||||
|
|
23
app/views/transactions/_filter.html.erb
Normal file
23
app/views/transactions/_filter.html.erb
Normal file
|
@ -0,0 +1,23 @@
|
|||
<%# locals: (filter:) %>
|
||||
<div class="flex items-center gap-1 text-sm border border-alpha-black-200 rounded-3xl p-1.5">
|
||||
<% if filter[:type] == "account" %>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-5 h-5 bg-blue-600/10 text-xs flex items-center justify-center rounded-full"><%= filter[:value].name[0].upcase %></div>
|
||||
<p><%= filter[:value].name %></p>
|
||||
</div>
|
||||
<% elsif filter[:type] == "search" %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= lucide_icon "text", class: "w-5 h-5 text-gray-500" %>
|
||||
<p><%= "\"#{filter[:value]}\"".truncate(20) %></p>
|
||||
</div>
|
||||
<% 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] %>
|
||||
<% end %>
|
||||
<%= form.button type: "submit", class: "hover:text-gray-900" do %>
|
||||
<%= lucide_icon "x", class: "w-4 h-4 text-gray-500" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
10
app/views/transactions/_filters.html.erb
Normal file
10
app/views/transactions/_filters.html.erb
Normal file
|
@ -0,0 +1,10 @@
|
|||
<%# locals: (filters:) %>
|
||||
<div>
|
||||
<%= turbo_frame_tag "transactions_filters" do %>
|
||||
<div class="flex items-center gap-2">
|
||||
<% filters.each do |filter| %>
|
||||
<%= render partial: "transactions/filter", locals: { filter: filter } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
34
app/views/transactions/_list.html.erb
Normal file
34
app/views/transactions/_list.html.erb
Normal file
|
@ -0,0 +1,34 @@
|
|||
<%# locals: (transactions:, pagy:) %>
|
||||
<div>
|
||||
<%= 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>
|
||||
<div class="w-48">
|
||||
<p class="uppercase">category</p>
|
||||
</div>
|
||||
<div class="grow uppercase flex justify-between items-center gap-5 text-xs font-medium text-gray-500">
|
||||
<p>account</p>
|
||||
<p>amount</p>
|
||||
</div>
|
||||
</div>
|
||||
<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 mt-4 sm:px-0">
|
||||
<%= render partial: "transactions/pagination", locals: { pagy: pagy } %>
|
||||
</nav>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
46
app/views/transactions/_search_form.html.erb
Normal file
46
app/views/transactions/_search_form.html.erb
Normal file
|
@ -0,0 +1,46 @@
|
|||
<%# locals: (q:) %>
|
||||
<div>
|
||||
<%= turbo_frame_tag "transactions_search_form" do %>
|
||||
<%= search_form_for @q, url: search_transactions_path, html: { method: :post, data: { turbo_frame: "transactions_list", "controller": "auto-submit-form" } } do |form| %>
|
||||
<div class="flex gap-2 mb-4">
|
||||
<div class="grow">
|
||||
<%= render partial: "transactions/search_form/search_filter", locals: { form: form } %>
|
||||
</div>
|
||||
<div data-controller="dropdown" class="relative">
|
||||
<button type="button" data-action="dropdown#toggleMenu" class="border border-gray-200 block h-full rounded-lg flex items-center gap-2 px-4">
|
||||
<%= lucide_icon("list-filter", class: "w-5 h-5 text-gray-500") %>
|
||||
<p class="text-sm font-medium text-gray-900">Filter</p>
|
||||
</button>
|
||||
<div class="hidden absolute z-10 top-12 right-0 border border-alpha-black-25 bg-white rounded-lg shadow-xs min-w-[450px]" data-dropdown-target="menu">
|
||||
<div data-controller="tabs" data-tabs-active-class="border-b-2 border-b-black text-gray-900" data-tabs-default-tab-value="txn-account-filter">
|
||||
<div class="flex items-center px-3 text-sm font-medium text-gray-500 gap-4 border-b border-b-alpha-black-50">
|
||||
<button class="py-2 border-b-2" type="button" data-id="txn-account-filter" data-tabs-target="btn" data-action="tabs#select">Account</button>
|
||||
<button class="py-2 border-b-2" type="button" data-id="txn-amount-filter" data-tabs-target="btn" data-action="tabs#select">Amount</button>
|
||||
<button class="py-2 border-b-2" type="button" data-id="txn-category-filter" data-tabs-target="btn" data-action="tabs#select">Category</button>
|
||||
<button class="py-2 border-b-2" type="button" data-id="txn-merchant-filter" data-tabs-target="btn" data-action="tabs#select">Merchant</button>
|
||||
</div>
|
||||
<div class="p-2 max-h-[300px] overflow-y-auto">
|
||||
<div id="txn-account-filter" data-tabs-target="tab">
|
||||
<%= render partial: "transactions/search_form/account_filter", locals: { form: form } %>
|
||||
</div>
|
||||
<div id="txn-amount-filter" data-tabs-target="tab">
|
||||
<%= render partial: "transactions/search_form/amount_filter", locals: { form: form } %>
|
||||
</div>
|
||||
<div id="txn-category-filter" data-tabs-target="tab">
|
||||
<%= render partial: "transactions/search_form/category_filter", locals: { form: form } %>
|
||||
</div>
|
||||
<div id="txn-merchant-filter" data-tabs-target="tab">
|
||||
<%= render partial: "transactions/search_form/merchant_filter", locals: { form: form } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm p-3 flex items-center justify-between border-t border-t-alpha-black-50">
|
||||
<p class="text-gray-500"><%= q.conditions.reject { |condition| condition.values.any?(&:blank?) }.size %> filters applied</p>
|
||||
<%= button_to "Clear all", search_transactions_path(clear: true), method: :post, class: "text-gray-900 font-medium", form: { "data-turbo-frame": "transactions_list" } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
21
app/views/transactions/_summary.html.erb
Normal file
21
app/views/transactions/_summary.html.erb
Normal file
|
@ -0,0 +1,21 @@
|
|||
<%# locals: (totals:) %>
|
||||
<%= turbo_frame_tag "transactions_summary" do %>
|
||||
<div class="grid grid-cols-3 bg-white rounded-xl border border-alpha-black-25 shadow-xs px-4 divide-x divide-alpha-black-100">
|
||||
<div class="p-4 space-y-2">
|
||||
<p class="text-sm text-gray-500">Total transactions</p>
|
||||
<p class="text-gray-900 font-medium text-xl"><%= totals[:count] %></p>
|
||||
</div>
|
||||
<div class="p-4 space-y-2">
|
||||
<p class="text-sm text-gray-500">Income</p>
|
||||
<p class="text-gray-900 font-medium text-xl">
|
||||
<%= format_money totals[:income] %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4 space-y-2">
|
||||
<p class="text-sm text-gray-500">Expenses</p>
|
||||
<p class="text-gray-900 font-medium text-xl">
|
||||
<%= format_money totals[:expense] %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
|
@ -2,90 +2,20 @@
|
|||
<div class="flex justify-between items-center text-gray-900 font-medium">
|
||||
<h1 class="text-xl">Transactions</h1>
|
||||
<div class="flex items-center gap-5">
|
||||
<div class="flex items-center gap-1 cursor-not-allowed">
|
||||
<span class="text-sm">USD $</span>
|
||||
<%= lucide_icon("chevron-down", class: "w-5 h-5 text-gray-500") %>
|
||||
</div>
|
||||
<div class="border-r border-alpha-black-200 h-5"></div>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= lucide_icon("settings-2", class: "cursor-not-allowed w-5 h-5 text-gray-500") %>
|
||||
<%= link_to new_transaction_path, class: "rounded-full w-9 h-9 bg-gray-900 text-white flex items-center justify-center hover:bg-gray-700" do %>
|
||||
<%= 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" do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<p>New transaction</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 bg-white rounded-xl border border-alpha-black-25 shadow-xs px-4 divide-x divide-alpha-black-100">
|
||||
<div class="p-4 space-y-2">
|
||||
<p class="text-sm text-gray-500">Total transactions</p>
|
||||
<p class="text-gray-900 font-medium text-xl"><%= @transactions.size %></p>
|
||||
</div>
|
||||
<div class="p-4 space-y-2">
|
||||
<p class="text-sm text-gray-500">Income</p>
|
||||
<p class="text-gray-900 font-medium text-xl">
|
||||
<%= format_money @transactions.inflows.sum(&:amount_money).abs %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4 space-y-2">
|
||||
<p class="text-sm text-gray-500">Expenses</p>
|
||||
<p class="text-gray-900 font-medium text-xl">
|
||||
<%= format_money @transactions.outflows.sum(&:amount_money) %>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<%= render partial: "transactions/summary", locals: { totals: @totals } %>
|
||||
</div>
|
||||
<div id="transactions" class="bg-white rounded-xl border border-alpha-black-25 shadow-xs p-4 space-y-4">
|
||||
<%= 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.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>
|
||||
</div>
|
||||
<div>
|
||||
<button class="cursor-not-allowed border border-gray-200 block h-full rounded-lg flex items-center gap-2 px-4">
|
||||
<%= lucide_icon("list-filter", class: "w-5 h-5 text-gray-500") %>
|
||||
<p class="text-sm font-medium text-gray-900">Filter</p>
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
<% 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>
|
||||
<div class="w-48">
|
||||
<p class="uppercase">category</p>
|
||||
</div>
|
||||
<div class="grow uppercase flex justify-between items-center gap-5 text-xs font-medium text-gray-500">
|
||||
<p>account</p>
|
||||
<p>amount</p>
|
||||
</div>
|
||||
</div>
|
||||
<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 mt-4 sm:px-0">
|
||||
<%= render partial: "transactions/pagination", locals: { pagy: @pagy } %>
|
||||
</nav>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= render partial: "transactions/search_form", locals: { q: @q } %>
|
||||
<%= render partial: "transactions/filters", locals: { filters: @filter_list } %>
|
||||
<%= render partial: "transactions/list", locals: { transactions: @transactions, pagy: @pagy } %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
15
app/views/transactions/search_form/_account_filter.html.erb
Normal file
15
app/views/transactions/search_form/_account_filter.html.erb
Normal file
|
@ -0,0 +1,15 @@
|
|||
<%# locals: (form:) %>
|
||||
<div data-controller="list-filter">
|
||||
<div class="relative">
|
||||
<input type="search" placeholder="Filter accounts" data-list-filter-target="input" data-action="input->list-filter#filter" class="block w-full border border-gray-200 rounded-md py-2 pl-10 pr-3 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
||||
<%= lucide_icon("search", class: "w-5 h-5 text-gray-500 absolute inset-y-0 left-2 top-1/2 transform -translate-y-1/2") %>
|
||||
</div>
|
||||
<div class="my-2" id="list" data-list-filter-target="list">
|
||||
<% Current.family.accounts.each do |account| %>
|
||||
<div class="filterable-item flex items-center gap-2 p-2" data-filter-name="<%= account.name %>">
|
||||
<%= form.check_box :account_id_in, { "data-auto-submit-form-target": "auto", multiple: true, id: dom_id(account), class: "rounded-sm border-gray-300 text-indigo-600 shadow-xs focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" }, account.id, nil %>
|
||||
<%= form.label :account_id_in, account.name, value: account.id, class: "text-sm text-gray-900" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,4 @@
|
|||
<%# locals: (form:) %>
|
||||
<div class="py-12 flex items-center justify-center">
|
||||
<p class="text-gray-500 text-sm">Coming soon...</p>
|
||||
</div>
|
|
@ -0,0 +1,4 @@
|
|||
<%# locals: (form:) %>
|
||||
<div class="py-12 flex items-center justify-center">
|
||||
<p class="text-gray-500 text-sm">Coming soon...</p>
|
||||
</div>
|
|
@ -0,0 +1,4 @@
|
|||
<%# locals: (form:) %>
|
||||
<div class="py-12 flex items-center justify-center">
|
||||
<p class="text-gray-500 text-sm">Coming soon...</p>
|
||||
</div>
|
|
@ -0,0 +1,8 @@
|
|||
<%# locals: (form:) %>
|
||||
<div class="relative flex items-center bg-white border border-gray-200 rounded-lg">
|
||||
<%= 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-auto-submit-form-target": "auto" %>
|
||||
<%= 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>
|
|
@ -13,7 +13,7 @@
|
|||
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
</div>
|
||||
</summary>
|
||||
<%= f.date_field :date, label: "Date" %>
|
||||
<%= f.date_field :date, label: "Date", "data-auto-submit-form-target": "auto" %>
|
||||
<div class="h-2"></div>
|
||||
<%= f.collection_select :account_id, Current.family.accounts, :id, :name, { prompt: "Select an Account", label: "Account", class: "text-gray-400" }, {class: "form-field__input cursor-not-allowed text-gray-400", disabled: "disabled"} %>
|
||||
</details>
|
||||
|
@ -25,7 +25,7 @@
|
|||
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
</div>
|
||||
</summary>
|
||||
<%= f.text_field :name, label: "Name" %>
|
||||
<%= f.text_field :name, label: "Name", "data-auto-submit-form-target": "auto" %>
|
||||
</details>
|
||||
<details class="group" open>
|
||||
<summary class="list-none bg-gray-25 rounded-xl py-1 mt-6 group-open:mb-2">
|
||||
|
@ -36,7 +36,7 @@
|
|||
</div>
|
||||
</summary>
|
||||
<label class="flex items-center cursor-pointer justify-between mx-3">
|
||||
<%= f.check_box :excluded, class: "sr-only peer" %>
|
||||
<%= f.check_box :excluded, class: "sr-only peer", "data-auto-submit-form-target": "auto" %>
|
||||
<div class="flex flex-col justify-center text-sm w-[340px] py-3">
|
||||
<span class="text-gray-900 mb-1">Exclude from analytics</span>
|
||||
<span class="text-gray-500">This excludes the transaction from any in-app features or analytics.</span>
|
||||
|
@ -52,7 +52,7 @@
|
|||
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
|
||||
</div>
|
||||
</summary>
|
||||
<%= f.text_area :notes, label: "Notes", placeholder: "Enter a note" %>
|
||||
<%= f.text_area :notes, label: "Notes", placeholder: "Enter a note", "data-auto-submit-form-target": "auto" %>
|
||||
</details>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
|
|
@ -7,7 +7,10 @@ Rails.application.routes.draw do
|
|||
resource :password
|
||||
resource :settings, only: %i[edit update]
|
||||
|
||||
resources :transactions
|
||||
resources :transactions do
|
||||
match "search" => "transactions#search", on: :collection, via: [ :get, :post ], as: :search
|
||||
end
|
||||
|
||||
resources :accounts, shallow: true do
|
||||
post :sync, on: :member
|
||||
resources :valuations
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue