Add boolean custom field filters to member overview closes #309 #362

Merged
simon merged 15 commits from feature/filter-boolean-custom-fields into main 2026-01-23 14:53:08 +01:00
2 changed files with 86 additions and 13 deletions
Showing only changes of commit 1d46fd1baf - Show all commits

View file

@ -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)}

View file

@ -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