From c0973c7f06b08cb1d17df5cf9acedf56ae0d994f Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 4 Mar 2026 20:46:31 +0100 Subject: [PATCH] Add member fee type filter to member list - Filter by membership fee type in same style as groups (All/Yes/No per type) - Index: load fee types, fee_type_filters, URL params, apply_fee_type_filters - MemberFilterComponent: fee types section, events, reset, button label - Refactor update_filters: extract parse/dispatch helpers to satisfy Credo complexity --- .../components/member_filter_component.ex | 269 ++++++++++++++---- lib/mv_web/live/member_live/index.ex | 197 ++++++++++++- lib/mv_web/live/member_live/index.html.heex | 2 + 3 files changed, 399 insertions(+), 69 deletions(-) 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-4"> +
+ {gettext("Fee types")} +
+
+
+ + {fee_type.name} + +
+ + + +
+
+
+
+
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)}