mirror of
https://github.com/we-promise/sure.git
synced 2026-04-10 15:54:48 +00:00
* Extract Entry.uncategorized_transactions scope, remove Family#uncategorized_transaction_count Adds a single Entry.uncategorized_transactions scope containing the shared conditions (transactions join, active accounts, category nil, not transfer kinds, not excluded). All callers now use this scope: - Entry.uncategorized_matching builds on it - Transaction::Grouper::ByMerchantOrName#uncategorized_entries uses it - categorizes_controller#uncategorized_entries_for uses it (also fixes missing status/excluded filters that were silently absent before) - Both controllers replace Current.family.uncategorized_transaction_count with Current.accessible_entries.uncategorized_transactions.count so the button count and wizard count both respect account sharing Family#uncategorized_transaction_count removed as it is now unused and was family-scoped rather than user-scoped. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Scope assign_entry write to Current.accessible_entries Replaces unscoped Entry.where(id:) with Current.accessible_entries.where(id:) so the write path is consistent with the find above it. Not exploitable given the find would 404 first, but removes the pattern inconsistency. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add privacy-sensitive class to amounts in categorize wizard Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Extract uncategorized_count helper in CategorizesController Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix comment on uncategorized_transactions scope to mention draft accounts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Use uncategorized_count helper in assign_entry action Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
135 lines
5.2 KiB
Ruby
135 lines
5.2 KiB
Ruby
class Transactions::CategorizesController < ApplicationController
|
|
def show
|
|
@breadcrumbs = [
|
|
[ t("breadcrumbs.home"), root_path ],
|
|
[ t("breadcrumbs.transactions"), transactions_path ],
|
|
[ t("breadcrumbs.categorize"), nil ]
|
|
]
|
|
@position = [ params[:position].to_i, 0 ].max
|
|
groups = Transaction::Grouper.strategy.call(
|
|
Current.accessible_entries,
|
|
limit: 1,
|
|
offset: @position
|
|
)
|
|
|
|
if groups.empty?
|
|
redirect_to transactions_path, notice: t(".all_done") and return
|
|
end
|
|
|
|
@group = groups.first
|
|
@categories = Current.family.categories.alphabetically
|
|
@total_uncategorized = uncategorized_count
|
|
end
|
|
|
|
def create
|
|
@position = params[:position].to_i
|
|
entry_ids = Array.wrap(params[:entry_ids]).reject(&:blank?)
|
|
all_entry_ids = Array.wrap(params[:all_entry_ids]).reject(&:blank?)
|
|
remaining_ids = all_entry_ids - entry_ids
|
|
|
|
category = Current.family.categories.find(params[:category_id])
|
|
entries = Current.accessible_entries.excluding_split_parents.where(id: entry_ids)
|
|
count = entries.bulk_update!({ category_id: category.id })
|
|
|
|
if params[:create_rule] == "1"
|
|
rule = Rule.create_from_grouping(
|
|
Current.family,
|
|
params[:grouping_key],
|
|
category,
|
|
transaction_type: params[:transaction_type]
|
|
)
|
|
flash[:alert] = t(".rule_creation_failed") if rule.nil?
|
|
end
|
|
|
|
respond_to do |format|
|
|
format.turbo_stream do
|
|
remaining_entries = uncategorized_entries_for(remaining_ids)
|
|
remaining_ids = remaining_entries.map { |e| e.id.to_s }
|
|
|
|
if remaining_ids.empty?
|
|
render turbo_stream: turbo_stream.action(:redirect, transactions_categorize_path(position: @position))
|
|
else
|
|
@categories = Current.family.categories.alphabetically
|
|
streams = entry_ids.map { |id| turbo_stream.remove("categorize_entry_#{id}") }
|
|
remaining_entries.each do |entry|
|
|
streams << turbo_stream.replace(
|
|
"categorize_entry_#{entry.id}",
|
|
partial: "transactions/categorizes/entry_row",
|
|
locals: { entry: entry, categories: @categories }
|
|
)
|
|
end
|
|
streams << turbo_stream.replace("categorize_remaining",
|
|
partial: "transactions/categorizes/remaining_count",
|
|
locals: { total_uncategorized: uncategorized_count })
|
|
streams << turbo_stream.replace("categorize_group_summary",
|
|
partial: "transactions/categorizes/group_summary",
|
|
locals: { entries: remaining_entries })
|
|
streams.concat(flash_notification_stream_items)
|
|
render turbo_stream: streams
|
|
end
|
|
end
|
|
format.html { redirect_to transactions_categorize_path(position: @position), notice: t(".categorized", count: count) }
|
|
end
|
|
end
|
|
|
|
def preview_rule
|
|
filter = params[:filter].to_s.strip
|
|
transaction_type = params[:transaction_type].presence
|
|
entries = filter.present? ? Entry.uncategorized_matching(Current.accessible_entries, filter, transaction_type) : []
|
|
@categories = Current.family.categories.alphabetically
|
|
|
|
render turbo_stream: [
|
|
turbo_stream.replace("categorize_group_title",
|
|
partial: "transactions/categorizes/group_title",
|
|
locals: { display_name: filter.presence || "…", color: "#737373", transaction_type: transaction_type }),
|
|
turbo_stream.replace("categorize_group_summary",
|
|
partial: "transactions/categorizes/group_summary",
|
|
locals: { entries: entries }),
|
|
turbo_stream.replace("categorize_transaction_list",
|
|
partial: "transactions/categorizes/transaction_list",
|
|
locals: { entries: entries, categories: @categories })
|
|
]
|
|
end
|
|
|
|
def assign_entry
|
|
entry = Current.accessible_entries.excluding_split_parents.find(params[:entry_id])
|
|
category = Current.family.categories.find(params[:category_id])
|
|
position = params[:position].to_i
|
|
all_entry_ids = Array.wrap(params[:all_entry_ids]).reject(&:blank?)
|
|
remaining_ids = all_entry_ids - [ entry.id.to_s ]
|
|
|
|
Current.accessible_entries.where(id: entry.id).bulk_update!({ category_id: category.id })
|
|
|
|
remaining_entries = uncategorized_entries_for(remaining_ids)
|
|
remaining_ids = remaining_entries.map { |e| e.id.to_s }
|
|
|
|
streams = [ turbo_stream.remove("categorize_entry_#{entry.id}") ]
|
|
if remaining_ids.empty?
|
|
streams << turbo_stream.action(:redirect, transactions_categorize_path(position: position))
|
|
else
|
|
streams << turbo_stream.replace("categorize_remaining",
|
|
partial: "transactions/categorizes/remaining_count",
|
|
locals: { total_uncategorized: uncategorized_count })
|
|
streams << turbo_stream.replace("categorize_group_summary",
|
|
partial: "transactions/categorizes/group_summary",
|
|
locals: { entries: remaining_entries })
|
|
end
|
|
render turbo_stream: streams
|
|
end
|
|
|
|
private
|
|
|
|
def uncategorized_count
|
|
Current.accessible_entries.uncategorized_transactions.count
|
|
end
|
|
|
|
def uncategorized_entries_for(ids)
|
|
return [] if ids.blank?
|
|
Current.accessible_entries
|
|
.excluding_split_parents
|
|
.where(id: ids)
|
|
.uncategorized_transactions
|
|
.to_a
|
|
end
|
|
end
|