Files
sure/app/controllers/categories_controller.rb
ghost 01d7361477 fix(categories): avoid index N+1 queries (#2089)
* fix(categories): avoid index N+1 queries

Precompute category hierarchy groups and transaction membership for the categories settings index so the view no longer checks subcategories and transaction existence per rendered row.

Add a regression test for the query shape and stabilize a date-sensitive account statement coverage test that blocked the full suite on month-end-adjacent dates.

* fix(categories): preserve partial transaction fallback

Keep category list rendering compatible with callers that omit precomputed transaction membership data. Passing nil now lets the category row partial use its existing transaction existence fallback instead of forcing direct delete actions.

Add a view regression test for the fallback path.

* test(categories): cover parent deletion child transactions

* Bad merge confilict fix

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
2026-06-01 21:59:46 +02:00

144 lines
4.4 KiB
Ruby

class CategoriesController < ApplicationController
before_action :set_category, only: %i[edit update destroy]
before_action :set_categories, only: %i[update edit]
before_action :set_transaction, only: :create
def index
@categories = Current.family.categories.alphabetically.to_a
@category_groups = Category::Group.for(@categories)
@category_ids_with_transactions = category_ids_with_transactions(@categories)
render layout: "settings"
end
def new
@category = Current.family.categories.new color: Category::COLORS.sample
set_categories
end
def merge
@categories = Current.family.categories.alphabetically
render layout: turbo_frame_request? ? false : "settings"
end
def create
@category = Current.family.categories.new(category_params)
if @category.save
@transaction.update(category_id: @category.id) if @transaction
flash[:notice] = t(".success")
redirect_target_url = request.referer || categories_path
respond_to do |format|
format.html { redirect_back_or_to categories_path, notice: t(".success") }
format.turbo_stream { render turbo_stream: turbo_stream.action(:redirect, redirect_target_url) }
end
else
set_categories
render :new, status: :unprocessable_entity
end
end
def edit
end
def update
if @category.update(category_params)
flash[:notice] = t(".success")
redirect_target_url = request.referer || categories_path
respond_to do |format|
format.html { redirect_back_or_to categories_path, notice: t(".success") }
format.turbo_stream { render turbo_stream: turbo_stream.action(:redirect, redirect_target_url) }
end
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@category.destroy
redirect_back_or_to categories_path, notice: t(".success")
end
def destroy_all
Current.family.categories.destroy_all
redirect_back_or_to categories_path, notice: t(".success")
end
def bootstrap
Current.family.categories.bootstrap!
redirect_back_or_to categories_path, notice: t(".success")
end
def perform_merge
permitted_params = category_merge_params
if permitted_params[:target_id].present? && Array(permitted_params[:source_ids]).include?(permitted_params[:target_id])
return redirect_to merge_categories_path, alert: t(".target_selected_as_source")
end
target = Current.family.categories.find_by(id: permitted_params[:target_id])
return redirect_to merge_categories_path, alert: t(".target_not_found") unless target
sources = Current.family.categories.where(id: permitted_params[:source_ids])
return redirect_to merge_categories_path, alert: t(".invalid_categories") unless sources.any?
merger = Category::Merger.new(family: Current.family, target_category: target, source_categories: sources)
return redirect_to merge_categories_path, alert: t(".no_categories_selected") unless merger.merge!
redirect_to categories_path, notice: t(".success", count: merger.merged_count)
rescue Category::Merger::UnauthorizedCategoryError => e
redirect_to merge_categories_path, alert: e.message
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotDestroyed => e
redirect_to merge_categories_path, alert: record_error_message(e)
end
private
def set_category
@category = Current.family.categories.find(params[:id])
end
def set_categories
@categories = unless @category.parent?
Current.family.categories.alphabetically.roots.where.not(id: @category.id)
else
[]
end
end
def set_transaction
if params[:transaction_id].present?
@transaction = Current.family.transactions.find(params[:transaction_id])
end
end
def category_params
params.require(:category).permit(:name, :color, :parent_id, :lucide_icon)
end
def category_merge_params
params.permit(:target_id, source_ids: [])
end
def category_ids_with_transactions(categories)
category_ids = categories.map(&:id)
return {} if category_ids.empty?
Current.family.transactions
.where(category_id: category_ids)
.distinct
.pluck(:category_id)
.index_with(true)
end
def record_error_message(error)
record = error.respond_to?(:record) ? error.record : nil
record&.errors&.full_messages&.to_sentence.presence || error.message
end
end