Add boolean custom field filters to member overview closes #309 #362
2 changed files with 86 additions and 13 deletions
|
|
@ -308,15 +308,9 @@ defmodule MvWeb.Components.MemberFilterComponent do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("reset_filters", _params, socket) do
|
def handle_event("reset_filters", _params, socket) do
|
||||||
# Reset payment filter
|
# Send single message to reset all filters at once (performance optimization)
|
||||||
if socket.assigns.cycle_status_filter != nil do
|
# This avoids N×2 load_members() calls when resetting multiple filters
|
||||||
send(self(), {:payment_filter_changed, nil})
|
send(self(), {:reset_all_filters, 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)
|
|
||||||
|
|
||||||
# Close dropdown after reset
|
# Close dropdown after reset
|
||||||
{:noreply, assign(socket, :open, false)}
|
{:noreply, assign(socket, :open, false)}
|
||||||
|
|
|
||||||
|
|
@ -434,6 +434,37 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
)}
|
)}
|
||||||
end
|
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
|
@impl true
|
||||||
def handle_info({:field_toggled, field_string, visible}, socket) do
|
def handle_info({:field_toggled, field_string, visible}, socket) do
|
||||||
# Update user field selection
|
# Update user field selection
|
||||||
|
|
@ -509,6 +540,9 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
"""
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
def handle_params(params, _url, socket) do
|
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
|
# Parse field selection from URL
|
||||||
url_selection = FieldSelection.parse_from_url(params)
|
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_member_fields = FieldVisibility.get_visible_member_fields(final_selection)
|
||||||
visible_custom_fields = FieldVisibility.get_visible_custom_fields(final_selection)
|
visible_custom_fields = FieldVisibility.get_visible_custom_fields(final_selection)
|
||||||
|
|
||||||
|
# Apply all updates
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> maybe_update_search(params)
|
|> maybe_update_search(params)
|
||||||
|
|
@ -543,13 +578,55 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
|> assign(:user_field_selection, final_selection)
|
|> assign(:user_field_selection, final_selection)
|
||||||
|> assign(:member_fields_visible, visible_member_fields)
|
|> assign(:member_fields_visible, visible_member_fields)
|
||||||
|> assign(:visible_custom_field_ids, extract_custom_field_ids(visible_custom_fields))
|
|> assign(:visible_custom_field_ids, extract_custom_field_ids(visible_custom_fields))
|
||||||
|> load_members()
|
|
||||||
|> prepare_dynamic_cols()
|
# Build signature AFTER updates
|
||||||
|> update_selection_assigns()
|
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}
|
{:noreply, socket}
|
||||||
end
|
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.
|
# Prepares dynamic column definitions for custom fields that should be shown in the overview.
|
||||||
#
|
#
|
||||||
# Creates a list of column definitions, each containing:
|
# 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
|
# Errors in handle_params are handled by Phoenix LiveView
|
||||||
actor = current_actor(socket)
|
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
|
# Custom field values are already filtered at the database level in load_custom_field_values/2
|
||||||
# No need for in-memory filtering anymore
|
# No need for in-memory filtering anymore
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue