diff --git a/lib/mv_web/live/components/member_filter_component.ex b/lib/mv_web/live/components/member_filter_component.ex index f5f99ea..9286ace 100644 --- a/lib/mv_web/live/components/member_filter_component.ex +++ b/lib/mv_web/live/components/member_filter_component.ex @@ -308,15 +308,9 @@ defmodule MvWeb.Components.MemberFilterComponent do @impl true def handle_event("reset_filters", _params, socket) do - # Reset payment filter - if socket.assigns.cycle_status_filter != nil do - send(self(), {:payment_filter_changed, nil}) - end - - # Reset all boolean filters - Enum.each(socket.assigns.boolean_filters, fn {custom_field_id_str, _value} -> - send(self(), {:boolean_filter_changed, custom_field_id_str, nil}) - end) + # Send single message to reset all filters at once (performance optimization) + # This avoids N×2 load_members() calls when resetting multiple filters + send(self(), {:reset_all_filters, nil, %{}}) # Close dropdown after reset {:noreply, assign(socket, :open, false)} diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 41ef275..1cb5d82 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -434,6 +434,37 @@ defmodule MvWeb.MemberLive.Index do )} end + @impl true + def handle_info({:reset_all_filters, cycle_status_filter, boolean_filters}, socket) do + # Reset all filters at once (performance optimization) + # This avoids N×2 load_members() calls when resetting multiple filters + socket = + socket + |> assign(:cycle_status_filter, cycle_status_filter) + |> assign(:boolean_custom_field_filters, boolean_filters) + |> load_members() + |> update_selection_assigns() + + # Build the URL with all params including reset filters + query_params = + build_query_params( + socket.assigns.query, + socket.assigns.sort_field, + socket.assigns.sort_order, + cycle_status_filter, + socket.assigns.show_current_cycle, + boolean_filters + ) + + new_path = ~p"/members?#{query_params}" + + {:noreply, + push_patch(socket, + to: new_path, + replace: true + )} + end + @impl true def handle_info({:field_toggled, field_string, visible}, socket) do # Update user field selection @@ -509,6 +540,9 @@ defmodule MvWeb.MemberLive.Index do """ @impl true def handle_params(params, _url, socket) do + # Build signature BEFORE updates to detect if anything actually changed + prev_sig = build_signature(socket) + # Parse field selection from URL url_selection = FieldSelection.parse_from_url(params) @@ -532,6 +566,7 @@ defmodule MvWeb.MemberLive.Index do visible_member_fields = FieldVisibility.get_visible_member_fields(final_selection) visible_custom_fields = FieldVisibility.get_visible_custom_fields(final_selection) + # Apply all updates socket = socket |> maybe_update_search(params) @@ -543,13 +578,55 @@ defmodule MvWeb.MemberLive.Index do |> assign(:user_field_selection, final_selection) |> assign(:member_fields_visible, visible_member_fields) |> assign(:visible_custom_field_ids, extract_custom_field_ids(visible_custom_fields)) - |> load_members() - |> prepare_dynamic_cols() - |> update_selection_assigns() + + # Build signature AFTER updates + next_sig = build_signature(socket) + + # Only load members if signature changed (optimization: avoid duplicate loads) + socket = + if prev_sig == next_sig do + # Nothing changed, skip expensive load_members() call + socket + |> prepare_dynamic_cols() + |> update_selection_assigns() + else + # Signature changed, reload members + socket + |> load_members() + |> prepare_dynamic_cols() + |> update_selection_assigns() + end {:noreply, socket} end + # Builds a signature tuple representing all filter/sort parameters that affect member loading. + # + # This signature is used to detect if member data needs to be reloaded when handle_params + # is called. If the signature hasn't changed, we can skip the expensive load_members() call. + # + # Returns a tuple containing all relevant parameters: + # - query: Search query string + # - sort_field: Field to sort by + # - sort_order: Sort direction (:asc or :desc) + # - cycle_status_filter: Payment filter (:paid, :unpaid, or nil) + # - show_current_cycle: Whether to show current cycle + # - boolean_custom_field_filters: Map of active boolean filters + # - user_field_selection: Map of user's field visibility selections + # - visible_custom_field_ids: List of visible custom field IDs (affects which custom fields are loaded) + defp build_signature(socket) do + { + socket.assigns.query, + socket.assigns.sort_field, + socket.assigns.sort_order, + socket.assigns.cycle_status_filter, + socket.assigns.show_current_cycle, + socket.assigns.boolean_custom_field_filters, + socket.assigns.user_field_selection, + socket.assigns[:visible_custom_field_ids] || [] + } + end + # Prepares dynamic column definitions for custom fields that should be shown in the overview. # # Creates a list of column definitions, each containing: @@ -823,7 +900,9 @@ defmodule MvWeb.MemberLive.Index do # Errors in handle_params are handled by Phoenix LiveView actor = current_actor(socket) - members = Ash.read!(query, actor: actor) + {time_microseconds, members} = :timer.tc(fn -> Ash.read!(query, actor: actor) end) + time_milliseconds = time_microseconds / 1000 + Logger.info("Ash.read! in load_members/1 took #{time_milliseconds} ms") # Custom field values are already filtered at the database level in load_custom_field_values/2 # No need for in-memory filtering anymore