diff --git a/lib/mv_web/live/components/member_filter_component.ex b/lib/mv_web/live/components/member_filter_component.ex
index 4a42bbc..56c5666 100644
--- a/lib/mv_web/live/components/member_filter_component.ex
+++ b/lib/mv_web/live/components/member_filter_component.ex
@@ -19,6 +19,8 @@ defmodule MvWeb.Components.MemberFilterComponent do
- `:groups` - List of groups (for per-group filter rows)
- `:group_filters` - Map of active group filters: `%{group_id => :in | :not_in}` (nil = All for that group).
Multiple active filters combine with AND (member must match all selected group conditions).
+ - `:fee_types` - List of membership fee types (for per-fee-type filter rows)
+ - `:fee_type_filters` - Map of active fee type filters: `%{fee_type_id => :in | :not_in}` (nil = All).
- `:boolean_custom_fields` - List of boolean custom fields to display
- `:boolean_filters` - Map of active boolean filters: `%{custom_field_id => true | false}`
- `:id` - Component ID (required)
@@ -27,11 +29,13 @@ defmodule MvWeb.Components.MemberFilterComponent do
## Events
- Sends `{:payment_filter_changed, filter}` to parent when payment filter changes
- Sends `{:group_filter_changed, group_id_str, value}` to parent when a group filter changes (value: nil | :in | :not_in)
+ - Sends `{:fee_type_filter_changed, fee_type_id_str, value}` to parent when a fee type filter changes (value: nil | :in | :not_in)
- Sends `{:boolean_filter_changed, custom_field_id, filter_value}` to parent when boolean filter changes
"""
use MvWeb, :live_component
@group_filter_prefix "group_"
+ @fee_type_filter_prefix "fee_type_"
@impl true
def mount(socket) do
@@ -47,6 +51,9 @@ defmodule MvWeb.Components.MemberFilterComponent do
|> assign(:groups, assigns[:groups] || [])
|> assign(:group_filters, assigns[:group_filters] || %{})
|> assign(:group_filter_prefix, @group_filter_prefix)
+ |> assign(:fee_types, assigns[:fee_types] || [])
+ |> assign(:fee_type_filters, assigns[:fee_type_filters] || %{})
+ |> assign(:fee_type_filter_prefix, @fee_type_filter_prefix)
|> assign(:boolean_custom_fields, assigns[:boolean_custom_fields] || [])
|> assign(:boolean_filters, assigns[:boolean_filters] || %{})
|> assign(:member_count, assigns[:member_count] || 0)
@@ -71,6 +78,7 @@ defmodule MvWeb.Components.MemberFilterComponent do
class={[
"gap-2",
(@cycle_status_filter || map_size(@group_filters) > 0 ||
+ map_size(@fee_type_filters) > 0 ||
active_boolean_filters_count(@boolean_filters) > 0) &&
"btn-active"
]}
@@ -86,6 +94,8 @@ defmodule MvWeb.Components.MemberFilterComponent do
@cycle_status_filter,
@groups,
@group_filters,
+ @fee_types,
+ @fee_type_filters,
@boolean_custom_fields,
@boolean_filters
)}
@@ -99,7 +109,7 @@ defmodule MvWeb.Components.MemberFilterComponent do
<.badge
:if={
- (@cycle_status_filter || map_size(@group_filters) > 0) &&
+ (@cycle_status_filter || map_size(@group_filters) > 0 || map_size(@fee_type_filters) > 0) &&
active_boolean_filters_count(@boolean_filters) == 0
}
variant="primary"
@@ -250,6 +260,73 @@ defmodule MvWeb.Components.MemberFilterComponent do
+
+
0} class="mb-2">
@@ -356,69 +433,21 @@ defmodule MvWeb.Components.MemberFilterComponent do
@impl true
def handle_event("update_filters", params, socket) do
- # Parse payment filter
- payment_filter =
- case Map.get(params, "payment_filter") do
- "paid" -> :paid
- "unpaid" -> :unpaid
- _ -> nil
- end
-
- # Parse per-group filters (params keys "group_" => "all"|"in"|"not_in")
- prefix_len = String.length(@group_filter_prefix)
+ payment_filter = parse_payment_filter(params)
group_filters_parsed =
- params
- |> Enum.filter(fn {key, _} -> String.starts_with?(key, @group_filter_prefix) end)
- |> Enum.reduce(%{}, fn {key, value_str}, acc ->
- group_id_str = String.slice(key, prefix_len, String.length(key) - prefix_len)
- filter_value = parse_group_filter_value(value_str)
- Map.put(acc, group_id_str, filter_value)
- end)
+ parse_prefix_filters(params, @group_filter_prefix, &parse_group_filter_value/1)
- # Parse boolean custom field filters (including nil values for "all")
- custom_boolean_filters_parsed =
- params
- |> Map.get("custom_boolean", %{})
- |> Enum.reduce(%{}, fn {custom_field_id_str, value_str}, acc ->
- filter_value = parse_tri_state(value_str)
- Map.put(acc, custom_field_id_str, filter_value)
- end)
+ fee_type_filters_parsed =
+ parse_prefix_filters(params, @fee_type_filter_prefix, &parse_fee_type_filter_value/1)
- # Update payment filter if changed
- if payment_filter != socket.assigns.cycle_status_filter do
- send(self(), {:payment_filter_changed, payment_filter})
- end
+ custom_boolean_filters_parsed = parse_custom_boolean_filters(params)
- # Update group filters - send event for each changed group
- current_group_filters = socket.assigns.group_filters
- all_group_ids = MapSet.new(Enum.map(socket.assigns.groups, &to_string(&1.id)))
+ dispatch_payment_filter_change(socket, payment_filter)
+ dispatch_group_filter_changes(socket, group_filters_parsed)
+ dispatch_fee_type_filter_changes(socket, fee_type_filters_parsed)
+ dispatch_boolean_filter_changes(socket, custom_boolean_filters_parsed)
- Enum.each(group_filters_parsed, fn {group_id_str, new_value} ->
- in_set = MapSet.member?(all_group_ids, group_id_str)
- current_value = Map.get(current_group_filters, group_id_str)
- should_send = in_set and current_value != new_value
-
- if should_send do
- send(self(), {:group_filter_changed, group_id_str, new_value})
- end
- end)
-
- # Update boolean filters - send events for each changed filter
- current_filters = socket.assigns.boolean_filters
-
- # Process all custom field filters from form (including those set to "all"/nil)
- # Radio buttons in a group always send a value, so all active filters are in the form
- Enum.each(custom_boolean_filters_parsed, fn {custom_field_id_str, new_value} ->
- current_value = Map.get(current_filters, custom_field_id_str)
-
- # Only send event if value actually changed
- if current_value != new_value do
- send(self(), {:boolean_filter_changed, custom_field_id_str, new_value})
- end
- end)
-
- # Don't close dropdown - allow multiple filter changes
{:noreply, socket}
end
@@ -426,7 +455,7 @@ defmodule MvWeb.Components.MemberFilterComponent do
def handle_event("reset_filters", _params, socket) do
# 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, %{}, %{}})
+ send(self(), {:reset_all_filters, nil, %{}, %{}, %{}})
# Close dropdown after reset
{:noreply, assign(socket, :open, false)}
@@ -442,11 +471,82 @@ defmodule MvWeb.Components.MemberFilterComponent do
defp parse_group_filter_value("not_in"), do: :not_in
defp parse_group_filter_value(_), do: nil
+ defp parse_fee_type_filter_value("in"), do: :in
+ defp parse_fee_type_filter_value("not_in"), do: :not_in
+ defp parse_fee_type_filter_value(_), do: nil
+
+ defp parse_payment_filter(params) do
+ case Map.get(params, "payment_filter") do
+ "paid" -> :paid
+ "unpaid" -> :unpaid
+ _ -> nil
+ end
+ end
+
+ defp parse_prefix_filters(params, prefix, parse_value_fn) do
+ prefix_len = String.length(prefix)
+
+ params
+ |> Enum.filter(fn {key, _} -> String.starts_with?(key, prefix) end)
+ |> Enum.reduce(%{}, fn {key, value_str}, acc ->
+ id_str = String.slice(key, prefix_len, String.length(key) - prefix_len)
+ Map.put(acc, id_str, parse_value_fn.(value_str))
+ end)
+ end
+
+ defp parse_custom_boolean_filters(params) do
+ params
+ |> Map.get("custom_boolean", %{})
+ |> Enum.reduce(%{}, fn {id_str, value_str}, acc ->
+ Map.put(acc, id_str, parse_tri_state(value_str))
+ end)
+ end
+
+ defp dispatch_payment_filter_change(socket, payment_filter) do
+ if payment_filter != socket.assigns.cycle_status_filter do
+ send(self(), {:payment_filter_changed, payment_filter})
+ end
+ end
+
+ defp dispatch_group_filter_changes(socket, group_filters_parsed) do
+ current = socket.assigns.group_filters
+ valid_ids = MapSet.new(Enum.map(socket.assigns.groups, &to_string(&1.id)))
+
+ Enum.each(group_filters_parsed, fn {id_str, new_value} ->
+ if MapSet.member?(valid_ids, id_str) and Map.get(current, id_str) != new_value do
+ send(self(), {:group_filter_changed, id_str, new_value})
+ end
+ end)
+ end
+
+ defp dispatch_fee_type_filter_changes(socket, fee_type_filters_parsed) do
+ current = socket.assigns.fee_type_filters
+ valid_ids = MapSet.new(Enum.map(socket.assigns.fee_types, &to_string(&1.id)))
+
+ Enum.each(fee_type_filters_parsed, fn {id_str, new_value} ->
+ if MapSet.member?(valid_ids, id_str) and Map.get(current, id_str) != new_value do
+ send(self(), {:fee_type_filter_changed, id_str, new_value})
+ end
+ end)
+ end
+
+ defp dispatch_boolean_filter_changes(socket, custom_boolean_filters_parsed) do
+ current = socket.assigns.boolean_filters
+
+ Enum.each(custom_boolean_filters_parsed, fn {id_str, new_value} ->
+ if Map.get(current, id_str) != new_value do
+ send(self(), {:boolean_filter_changed, id_str, new_value})
+ end
+ end)
+ end
+
# Get display label for button
defp button_label(
cycle_status_filter,
groups,
group_filters,
+ fee_types,
+ fee_type_filters,
boolean_custom_fields,
boolean_filters
) do
@@ -457,6 +557,9 @@ defmodule MvWeb.Components.MemberFilterComponent do
map_size(group_filters) > 0 ->
group_filters_label(groups, group_filters)
+ map_size(fee_type_filters) > 0 ->
+ fee_type_filters_label(fee_types, fee_type_filters)
+
map_size(boolean_filters) > 0 ->
boolean_filter_label(boolean_custom_fields, boolean_filters)
@@ -480,6 +583,21 @@ defmodule MvWeb.Components.MemberFilterComponent do
truncate_label(label, 30)
end
+ defp fee_type_filters_label(_fee_types, fee_type_filters) when map_size(fee_type_filters) == 0,
+ do: gettext("All")
+
+ defp fee_type_filters_label(fee_types, fee_type_filters) do
+ fee_types_by_id = Map.new(fee_types, fn ft -> {to_string(ft.id), ft.name} end)
+
+ names =
+ fee_type_filters
+ |> Enum.map(fn {fee_type_id_str, _} -> Map.get(fee_types_by_id, fee_type_id_str) end)
+ |> Enum.reject(&is_nil/1)
+
+ label = Enum.join(names, ", ")
+ truncate_label(label, 30)
+ end
+
# Get payment filter label
defp payment_filter_label(nil), do: gettext("All")
defp payment_filter_label(:paid), do: gettext("Paid")
@@ -586,6 +704,39 @@ defmodule MvWeb.Components.MemberFilterComponent do
end
end
+ # Get CSS classes for per-fee-type filter label based on current state
+ defp fee_type_filter_label_class(fee_type_filters, fee_type_id, expected_value) do
+ base_classes = "join-item btn btn-sm"
+ current_value = Map.get(fee_type_filters, to_string(fee_type_id))
+ is_active = current_value == expected_value
+
+ cond do
+ expected_value == nil ->
+ if is_active do
+ "#{base_classes} btn-active"
+ else
+ "#{base_classes} btn"
+ end
+
+ expected_value == :in ->
+ if is_active do
+ "#{base_classes} btn-success btn-active"
+ else
+ "#{base_classes} btn"
+ end
+
+ expected_value == :not_in ->
+ if is_active do
+ "#{base_classes} btn-error btn-active"
+ else
+ "#{base_classes} btn"
+ end
+
+ true ->
+ "#{base_classes} btn-outline"
+ end
+ end
+
# Get CSS classes for boolean filter label based on current state
defp boolean_filter_label_class(boolean_filters, custom_field_id, expected_value) do
base_classes = "join-item btn btn-sm"
diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex
index 6cf532d..c745a3d 100644
--- a/lib/mv_web/live/member_live/index.ex
+++ b/lib/mv_web/live/member_live/index.ex
@@ -33,6 +33,8 @@ defmodule MvWeb.MemberLive.Index do
alias Mv.Membership
alias Mv.Membership.Member, as: MemberResource
+ alias Mv.MembershipFees
+ alias Mv.MembershipFees.MembershipFeeType
alias MvWeb.Helpers.DateFormatter
alias MvWeb.MemberLive.Index.FieldSelection
alias MvWeb.MemberLive.Index.FieldVisibility
@@ -42,6 +44,7 @@ defmodule MvWeb.MemberLive.Index do
@custom_field_prefix Mv.Constants.custom_field_prefix()
@boolean_filter_prefix Mv.Constants.boolean_filter_prefix()
@group_filter_prefix "group_"
+ @fee_type_filter_prefix "fee_type_"
# Maximum number of boolean custom field filters allowed per request (DoS protection)
@max_boolean_filters Mv.Constants.max_boolean_filters()
@@ -89,6 +92,12 @@ defmodule MvWeb.MemberLive.Index do
|> Ash.Query.sort(name: :asc)
|> Ash.read!(actor: actor)
+ # Load membership fee types for filter dropdown (sorted by name)
+ fee_types =
+ MembershipFeeType
+ |> Ash.Query.sort(name: :asc)
+ |> Ash.read!(domain: MembershipFees, actor: actor)
+
# Load settings once to avoid N+1 queries
settings =
case Membership.get_settings() do
@@ -121,6 +130,8 @@ defmodule MvWeb.MemberLive.Index do
|> assign(:cycle_status_filter, nil)
|> assign(:group_filters, %{})
|> assign(:groups, groups)
+ |> assign(:fee_type_filters, %{})
+ |> assign(:fee_types, fee_types)
|> assign(:boolean_custom_field_filters, %{})
|> assign(:selected_members, MapSet.new())
|> assign(:selected_member_id, nil)
@@ -218,7 +229,8 @@ defmodule MvWeb.MemberLive.Index do
socket.assigns.cycle_status_filter,
socket.assigns[:group_filters],
new_show_current,
- socket.assigns.boolean_custom_field_filters
+ socket.assigns.boolean_custom_field_filters,
+ socket.assigns[:fee_type_filters]
)
|> maybe_add_field_selection(
socket.assigns[:user_field_selection],
@@ -300,7 +312,8 @@ defmodule MvWeb.MemberLive.Index do
socket.assigns.cycle_status_filter,
socket.assigns[:group_filters],
socket.assigns.show_current_cycle,
- socket.assigns.boolean_custom_field_filters
+ socket.assigns.boolean_custom_field_filters,
+ socket.assigns[:fee_type_filters]
)
|> maybe_add_field_selection(
socket.assigns[:user_field_selection],
@@ -339,7 +352,8 @@ defmodule MvWeb.MemberLive.Index do
socket.assigns.cycle_status_filter,
socket.assigns[:group_filters],
socket.assigns.show_current_cycle,
- socket.assigns.boolean_custom_field_filters
+ socket.assigns.boolean_custom_field_filters,
+ socket.assigns[:fee_type_filters]
)
|> maybe_add_field_selection(
socket.assigns[:user_field_selection],
@@ -367,7 +381,8 @@ defmodule MvWeb.MemberLive.Index do
filter,
socket.assigns[:group_filters],
socket.assigns.show_current_cycle,
- socket.assigns.boolean_custom_field_filters
+ socket.assigns.boolean_custom_field_filters,
+ socket.assigns[:fee_type_filters]
)
|> maybe_add_field_selection(
socket.assigns[:user_field_selection],
@@ -401,7 +416,8 @@ defmodule MvWeb.MemberLive.Index do
socket.assigns.cycle_status_filter,
socket.assigns[:group_filters],
socket.assigns.show_current_cycle,
- updated_filters
+ updated_filters,
+ socket.assigns[:fee_type_filters]
)
|> maybe_add_field_selection(
socket.assigns[:user_field_selection],
@@ -437,7 +453,45 @@ defmodule MvWeb.MemberLive.Index do
socket.assigns.cycle_status_filter,
group_filters,
socket.assigns.show_current_cycle,
- socket.assigns.boolean_custom_field_filters
+ socket.assigns.boolean_custom_field_filters,
+ socket.assigns[:fee_type_filters]
+ )
+ |> maybe_add_field_selection(
+ socket.assigns[:user_field_selection],
+ socket.assigns[:fields_in_url?] || false
+ )
+
+ new_path = ~p"/members?#{query_params}"
+ {:noreply, push_patch(socket, to: new_path, replace: true)}
+ end
+
+ @impl true
+ def handle_info({:fee_type_filter_changed, fee_type_id_str, filter_value}, socket) do
+ normalized_id = normalize_uuid_string(fee_type_id_str) || fee_type_id_str
+
+ fee_type_filters =
+ if filter_value == nil do
+ Map.delete(socket.assigns.fee_type_filters, normalized_id)
+ else
+ Map.put(socket.assigns.fee_type_filters, normalized_id, filter_value)
+ end
+
+ socket =
+ socket
+ |> assign(:fee_type_filters, fee_type_filters)
+ |> load_members()
+ |> update_selection_assigns()
+
+ query_params =
+ build_query_params(
+ socket.assigns.query,
+ socket.assigns.sort_field,
+ socket.assigns.sort_order,
+ socket.assigns.cycle_status_filter,
+ socket.assigns[:group_filters],
+ socket.assigns.show_current_cycle,
+ socket.assigns.boolean_custom_field_filters,
+ fee_type_filters
)
|> maybe_add_field_selection(
socket.assigns[:user_field_selection],
@@ -450,17 +504,29 @@ defmodule MvWeb.MemberLive.Index do
@impl true
def handle_info({:reset_all_filters, cycle_status_filter, boolean_filters}, socket) do
- handle_info({:reset_all_filters, cycle_status_filter, boolean_filters, %{}}, socket)
+ handle_info({:reset_all_filters, cycle_status_filter, boolean_filters, %{}, %{}}, socket)
end
def handle_info(
{:reset_all_filters, cycle_status_filter, boolean_filters, group_filters},
socket
) do
+ handle_info(
+ {:reset_all_filters, cycle_status_filter, boolean_filters, group_filters, %{}},
+ socket
+ )
+ end
+
+ def handle_info(
+ {:reset_all_filters, cycle_status_filter, boolean_filters, group_filters,
+ fee_type_filters},
+ socket
+ ) do
socket =
socket
|> assign(:cycle_status_filter, cycle_status_filter)
|> assign(:group_filters, group_filters)
+ |> assign(:fee_type_filters, fee_type_filters)
|> assign(:boolean_custom_field_filters, boolean_filters)
|> load_members()
|> update_selection_assigns()
@@ -473,7 +539,8 @@ defmodule MvWeb.MemberLive.Index do
cycle_status_filter,
socket.assigns[:group_filters],
socket.assigns.show_current_cycle,
- boolean_filters
+ boolean_filters,
+ fee_type_filters
)
|> maybe_add_field_selection(
socket.assigns[:user_field_selection],
@@ -598,6 +665,7 @@ defmodule MvWeb.MemberLive.Index do
|> maybe_update_sort(params)
|> maybe_update_cycle_status_filter(params)
|> maybe_update_group_filters(params)
+ |> maybe_update_fee_type_filters(params)
|> maybe_update_boolean_filters(params)
|> maybe_update_show_current_cycle(params)
|> assign(:fields_in_url?, fields_in_url?)
@@ -646,6 +714,7 @@ defmodule MvWeb.MemberLive.Index do
socket.assigns.sort_order,
socket.assigns.cycle_status_filter,
socket.assigns[:group_filters],
+ socket.assigns[:fee_type_filters],
socket.assigns.show_current_cycle,
socket.assigns.boolean_custom_field_filters,
socket.assigns.user_field_selection,
@@ -739,7 +808,8 @@ defmodule MvWeb.MemberLive.Index do
socket.assigns.cycle_status_filter,
socket.assigns[:group_filters],
socket.assigns.show_current_cycle,
- socket.assigns.boolean_custom_field_filters
+ socket.assigns.boolean_custom_field_filters,
+ socket.assigns[:fee_type_filters]
)
|> maybe_add_field_selection(socket.assigns[:user_field_selection], true)
@@ -758,15 +828,24 @@ defmodule MvWeb.MemberLive.Index do
cycle_status_filter,
group_filters,
show_current_cycle,
- boolean_filters
+ boolean_filters,
+ fee_type_filters
) do
base_params = build_base_params(query, sort_field, sort_order)
base_params = add_cycle_status_filter(base_params, cycle_status_filter)
base_params = add_group_filters(base_params, group_filters)
+ base_params = add_fee_type_filters(base_params, fee_type_filters || %{})
base_params = add_show_current_cycle(base_params, show_current_cycle)
add_boolean_filters(base_params, boolean_filters)
end
+ defp add_fee_type_filters(params, fee_type_filters) do
+ Enum.reduce(fee_type_filters, params, fn {fee_type_id_str, value}, acc ->
+ param_value = if value == :in, do: "in", else: "not_in"
+ Map.put(acc, "#{@fee_type_filter_prefix}#{fee_type_id_str}", param_value)
+ end)
+ end
+
defp compute_final_field_selection(true, url_selection, socket) do
only_url =
FieldVisibility.selection_from_url_only(url_selection, socket.assigns.all_custom_fields)
@@ -941,6 +1020,9 @@ defmodule MvWeb.MemberLive.Index do
query = apply_group_filters(query, socket.assigns[:group_filters], socket.assigns[:groups])
+ query =
+ apply_fee_type_filters(query, socket.assigns[:fee_type_filters], socket.assigns[:fee_types])
+
# Use ALL custom fields for sorting (not just show_in_overview subset)
custom_fields_for_sort = socket.assigns.all_custom_fields
@@ -1064,6 +1146,55 @@ defmodule MvWeb.MemberLive.Index do
defp apply_one_group_filter(query, _, _), do: query
+ # Multiple fee type filters combine with AND: member must match all selected fee type conditions.
+ defp apply_fee_type_filters(query, fee_type_filters, _fee_types) when fee_type_filters == %{},
+ do: query
+
+ defp apply_fee_type_filters(query, fee_type_filters, fee_types) do
+ valid_ids =
+ fee_types
+ |> Enum.map(&normalize_uuid_string(to_string(&1.id)))
+ |> Enum.reject(&is_nil/1)
+ |> MapSet.new()
+
+ Enum.reduce(fee_type_filters, query, fn {fee_type_id_str, value}, q ->
+ member? = MapSet.member?(valid_ids, fee_type_id_str)
+
+ if member? do
+ apply_one_fee_type_filter(q, fee_type_id_str, value)
+ else
+ q
+ end
+ end)
+ end
+
+ defp apply_one_fee_type_filter(query, _fee_type_id_str, nil), do: query
+
+ defp apply_one_fee_type_filter(query, fee_type_id_str, :in) do
+ case Ecto.UUID.cast(fee_type_id_str) do
+ {:ok, fee_type_uuid} ->
+ Ash.Query.filter(query, expr(membership_fee_type_id == ^fee_type_uuid))
+
+ _ ->
+ query
+ end
+ end
+
+ defp apply_one_fee_type_filter(query, fee_type_id_str, :not_in) do
+ case Ecto.UUID.cast(fee_type_id_str) do
+ {:ok, fee_type_uuid} ->
+ Ash.Query.filter(
+ query,
+ expr(membership_fee_type_id != ^fee_type_uuid or is_nil(membership_fee_type_id))
+ )
+
+ _ ->
+ query
+ end
+ end
+
+ defp apply_one_fee_type_filter(query, _, _), do: query
+
defp apply_cycle_status_filter(members, nil, _show_current), do: members
defp apply_cycle_status_filter(members, status, show_current)
@@ -1397,6 +1528,52 @@ defmodule MvWeb.MemberLive.Index do
defp maybe_update_group_filters(socket, _), do: socket
+ defp maybe_update_fee_type_filters(socket, params) when is_map(params) do
+ prefix = @fee_type_filter_prefix
+ prefix_len = String.length(prefix)
+
+ fee_type_param_entries =
+ params
+ |> Enum.filter(fn {key, _} ->
+ key_str = to_string(key)
+ String.starts_with?(key_str, prefix)
+ end)
+
+ filters =
+ Enum.reduce(fee_type_param_entries, %{}, fn {key, value_str}, acc ->
+ add_fee_type_filter_entry(acc, key, value_str, prefix_len)
+ end)
+
+ assign(socket, :fee_type_filters, filters)
+ end
+
+ defp maybe_update_fee_type_filters(socket, _), do: socket
+
+ defp add_fee_type_filter_entry(acc, key, value_str, prefix_len) do
+ key_str = to_string(key)
+ raw_id = String.slice(key_str, prefix_len, String.length(key_str) - prefix_len)
+ fee_type_id_str = normalize_uuid_string(raw_id)
+ valid_id? = fee_type_id_str && String.length(fee_type_id_str) <= @max_uuid_length
+
+ if valid_id? do
+ case parse_fee_type_filter_value(value_str) do
+ nil -> acc
+ value -> Map.put(acc, fee_type_id_str, value)
+ end
+ else
+ acc
+ end
+ end
+
+ defp parse_fee_type_filter_value("in"), do: :in
+ defp parse_fee_type_filter_value("not_in"), do: :not_in
+
+ defp parse_fee_type_filter_value(val) when is_binary(val) do
+ parse_fee_type_filter_value(String.trim(val))
+ end
+
+ defp parse_fee_type_filter_value(_), do: nil
+
defp add_group_filter_entry(acc, key, value_str, prefix_len) do
key_str = to_string(key)
raw_id = String.slice(key_str, prefix_len, String.length(key_str) - prefix_len)
diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex
index 84167c4..b35d426 100644
--- a/lib/mv_web/live/member_live/index.html.heex
+++ b/lib/mv_web/live/member_live/index.html.heex
@@ -50,6 +50,8 @@
cycle_status_filter={@cycle_status_filter}
groups={@groups}
group_filters={@group_filters}
+ fee_types={@fee_types}
+ fee_type_filters={@fee_type_filters}
boolean_custom_fields={@boolean_custom_fields}
boolean_filters={@boolean_custom_field_filters}
member_count={length(@members)}