Apply review feedback and fix Credo in fee type filter
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing

- Index: use FilterParams and constants; fix parse recursion; validate fee type/group
  IDs; OR semantics for :in; build_query_params/reset_all_filters map-based API;
  alias order (Credo); Map.take list deprecation fix
- MemberFilterComponent: use FilterParams and constants; fee_type_filter_part
  helper (Credo nesting); in_not_in_filter_label_class; reset_all_filters map;
  button label for :not_in and combined filter count; fieldset borders
- Gettext: Fee types, filter count plural, 'without %{name}' (en/de)
This commit is contained in:
Moritz 2026-03-09 14:28:50 +01:00 committed by moritz
parent ae07e3efc2
commit 8da22b3d88
5 changed files with 233 additions and 245 deletions

View file

@ -34,8 +34,10 @@ defmodule MvWeb.Components.MemberFilterComponent do
""" """
use MvWeb, :live_component use MvWeb, :live_component
@group_filter_prefix "group_" alias MvWeb.MemberLive.Index.FilterParams
@fee_type_filter_prefix "fee_type_"
@group_filter_prefix Mv.Constants.group_filter_prefix()
@fee_type_filter_prefix Mv.Constants.fee_type_filter_prefix()
@impl true @impl true
def mount(socket) do def mount(socket) do
@ -201,7 +203,7 @@ defmodule MvWeb.Components.MemberFilterComponent do
<div class="max-h-60 overflow-y-auto pr-2"> <div class="max-h-60 overflow-y-auto pr-2">
<fieldset <fieldset
:for={group <- @groups} :for={group <- @groups}
class="grid grid-cols-[1fr_auto] items-center gap-3 py-2 border-b border-base-200 last:border-0 border-0 p-0 m-0 min-w-0" class="grid grid-cols-[1fr_auto] items-center gap-3 py-2 border-0 border-b border-base-200 last:border-b-0 p-0 m-0 min-w-0"
> >
<legend class="text-sm font-medium col-start-1 float-left w-auto"> <legend class="text-sm font-medium col-start-1 float-left w-auto">
{group.name} {group.name}
@ -268,7 +270,7 @@ defmodule MvWeb.Components.MemberFilterComponent do
<div class="max-h-60 overflow-y-auto pr-2"> <div class="max-h-60 overflow-y-auto pr-2">
<fieldset <fieldset
:for={fee_type <- @fee_types} :for={fee_type <- @fee_types}
class="grid grid-cols-[1fr_auto] items-center gap-3 py-2 border-b border-base-200 last:border-0 border-0 p-0 m-0 min-w-0" class="grid grid-cols-[1fr_auto] items-center gap-3 py-2 border-0 border-b border-base-200 last:border-b-0 p-0 m-0 min-w-0"
> >
<legend class="text-sm font-medium col-start-1 float-left w-auto"> <legend class="text-sm font-medium col-start-1 float-left w-auto">
{fee_type.name} {fee_type.name}
@ -335,7 +337,7 @@ defmodule MvWeb.Components.MemberFilterComponent do
<div class="max-h-60 overflow-y-auto pr-2"> <div class="max-h-60 overflow-y-auto pr-2">
<fieldset <fieldset
:for={custom_field <- @boolean_custom_fields} :for={custom_field <- @boolean_custom_fields}
class="grid grid-cols-[1fr_auto] items-center gap-3 py-2 border-b border-base-200 last:border-0 border-0 p-0 m-0 min-w-0" class="grid grid-cols-[1fr_auto] items-center gap-3 py-2 border-0 border-b border-base-200 last:border-b-0 p-0 m-0 min-w-0"
> >
<legend class="text-sm font-medium col-start-1 float-left w-auto"> <legend class="text-sm font-medium col-start-1 float-left w-auto">
{custom_field.name} {custom_field.name}
@ -436,10 +438,10 @@ defmodule MvWeb.Components.MemberFilterComponent do
payment_filter = parse_payment_filter(params) payment_filter = parse_payment_filter(params)
group_filters_parsed = group_filters_parsed =
parse_prefix_filters(params, @group_filter_prefix, &parse_group_filter_value/1) parse_prefix_filters(params, @group_filter_prefix, &FilterParams.parse_in_not_in_value/1)
fee_type_filters_parsed = fee_type_filters_parsed =
parse_prefix_filters(params, @fee_type_filter_prefix, &parse_fee_type_filter_value/1) parse_prefix_filters(params, @fee_type_filter_prefix, &FilterParams.parse_in_not_in_value/1)
custom_boolean_filters_parsed = parse_custom_boolean_filters(params) custom_boolean_filters_parsed = parse_custom_boolean_filters(params)
@ -455,7 +457,16 @@ defmodule MvWeb.Components.MemberFilterComponent do
def handle_event("reset_filters", _params, socket) do def handle_event("reset_filters", _params, socket) do
# Send single message to reset all filters at once (performance optimization) # Send single message to reset all filters at once (performance optimization)
# This avoids N×2 load_members() calls when resetting multiple filters # This avoids N×2 load_members() calls when resetting multiple filters
send(self(), {:reset_all_filters, nil, %{}, %{}, %{}}) send(
self(),
{:reset_all_filters,
%{
cycle_status_filter: nil,
boolean_filters: %{},
group_filters: %{},
fee_type_filters: %{}
}}
)
# Close dropdown after reset # Close dropdown after reset
{:noreply, assign(socket, :open, false)} {:noreply, assign(socket, :open, false)}
@ -467,14 +478,6 @@ defmodule MvWeb.Components.MemberFilterComponent do
defp parse_tri_state("all"), do: nil defp parse_tri_state("all"), do: nil
defp parse_tri_state(_), do: nil defp parse_tri_state(_), do: nil
defp parse_group_filter_value("in"), do: :in
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 defp parse_payment_filter(params) do
case Map.get(params, "payment_filter") do case Map.get(params, "payment_filter") do
"paid" -> :paid "paid" -> :paid
@ -550,6 +553,19 @@ defmodule MvWeb.Components.MemberFilterComponent do
boolean_custom_fields, boolean_custom_fields,
boolean_filters boolean_filters
) do ) do
active_count =
count_active_filter_categories(
cycle_status_filter,
group_filters,
fee_type_filters,
boolean_filters
)
if active_count >= 2 do
ngettext("%{count} filter active", "%{count} filters active", active_count,
count: active_count
)
else
cond do cond do
cycle_status_filter -> cycle_status_filter ->
payment_filter_label(cycle_status_filter) payment_filter_label(cycle_status_filter)
@ -567,6 +583,22 @@ defmodule MvWeb.Components.MemberFilterComponent do
gettext("Apply filters") gettext("Apply filters")
end end
end end
end
defp count_active_filter_categories(
cycle_status_filter,
group_filters,
fee_type_filters,
boolean_filters
) do
[
cycle_status_filter,
map_size(group_filters) > 0,
map_size(fee_type_filters) > 0,
map_size(boolean_filters) > 0
]
|> Enum.count(& &1)
end
defp group_filters_label(_groups, group_filters) when map_size(group_filters) == 0, defp group_filters_label(_groups, group_filters) when map_size(group_filters) == 0,
do: gettext("All") do: gettext("All")
@ -589,15 +621,22 @@ defmodule MvWeb.Components.MemberFilterComponent do
defp fee_type_filters_label(fee_types, fee_type_filters) do 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) fee_types_by_id = Map.new(fee_types, fn ft -> {to_string(ft.id), ft.name} end)
names = parts =
fee_type_filters fee_type_filters
|> Enum.map(fn {fee_type_id_str, _} -> Map.get(fee_types_by_id, fee_type_id_str) end) |> Enum.map(fn {fee_type_id_str, value} ->
fee_type_filter_part(Map.get(fee_types_by_id, fee_type_id_str), value)
end)
|> Enum.reject(&is_nil/1) |> Enum.reject(&is_nil/1)
label = Enum.join(names, ", ") label = Enum.join(parts, ", ")
truncate_label(label, 30) truncate_label(label, 30)
end end
defp fee_type_filter_part(nil, _value), do: nil
defp fee_type_filter_part(name, :not_in), do: gettext("without %{name}", name: name)
defp fee_type_filter_part(name, _), do: name
# Get payment filter label # Get payment filter label
defp payment_filter_label(nil), do: gettext("All") defp payment_filter_label(nil), do: gettext("All")
defp payment_filter_label(:paid), do: gettext("Paid") defp payment_filter_label(:paid), do: gettext("Paid")
@ -671,70 +710,27 @@ defmodule MvWeb.Components.MemberFilterComponent do
end end
end end
# Get CSS classes for per-group filter label based on current state # Shared CSS classes for in/not_in filter labels (groups and fee types)
defp in_not_in_filter_label_class(filters, id, expected_value) do
base_classes = "join-item btn btn-sm"
current_value = Map.get(filters, to_string(id))
is_active = current_value == expected_value
case {expected_value, is_active} do
{_, false} -> "#{base_classes} btn"
{nil, true} -> "#{base_classes} btn-active"
{:in, true} -> "#{base_classes} btn-success btn-active"
{:not_in, true} -> "#{base_classes} btn-error btn-active"
_ -> "#{base_classes} btn-outline"
end
end
defp group_filter_label_class(group_filters, group_id, expected_value) do defp group_filter_label_class(group_filters, group_id, expected_value) do
base_classes = "join-item btn btn-sm" in_not_in_filter_label_class(group_filters, group_id, expected_value)
current_value = Map.get(group_filters, to_string(group_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 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 per-fee-type filter label based on current state
defp fee_type_filter_label_class(fee_type_filters, fee_type_id, expected_value) do defp fee_type_filter_label_class(fee_type_filters, fee_type_id, expected_value) do
base_classes = "join-item btn btn-sm" in_not_in_filter_label_class(fee_type_filters, fee_type_id, expected_value)
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 end
# Get CSS classes for boolean filter label based on current state # Get CSS classes for boolean filter label based on current state

View file

@ -38,13 +38,14 @@ defmodule MvWeb.MemberLive.Index do
alias MvWeb.Helpers.DateFormatter alias MvWeb.Helpers.DateFormatter
alias MvWeb.MemberLive.Index.FieldSelection alias MvWeb.MemberLive.Index.FieldSelection
alias MvWeb.MemberLive.Index.FieldVisibility alias MvWeb.MemberLive.Index.FieldVisibility
alias MvWeb.MemberLive.Index.FilterParams
alias MvWeb.MemberLive.Index.Formatter alias MvWeb.MemberLive.Index.Formatter
alias MvWeb.MemberLive.Index.MembershipFeeStatus alias MvWeb.MemberLive.Index.MembershipFeeStatus
@custom_field_prefix Mv.Constants.custom_field_prefix() @custom_field_prefix Mv.Constants.custom_field_prefix()
@boolean_filter_prefix Mv.Constants.boolean_filter_prefix() @boolean_filter_prefix Mv.Constants.boolean_filter_prefix()
@group_filter_prefix "group_" @group_filter_prefix Mv.Constants.group_filter_prefix()
@fee_type_filter_prefix "fee_type_" @fee_type_filter_prefix Mv.Constants.fee_type_filter_prefix()
# Maximum number of boolean custom field filters allowed per request (DoS protection) # Maximum number of boolean custom field filters allowed per request (DoS protection)
@max_boolean_filters Mv.Constants.max_boolean_filters() @max_boolean_filters Mv.Constants.max_boolean_filters()
@ -222,16 +223,7 @@ defmodule MvWeb.MemberLive.Index do
|> update_selection_assigns() |> update_selection_assigns()
query_params = query_params =
build_query_params( build_query_params(opts_for_query_params(socket, %{show_current_cycle: new_show_current}))
socket.assigns.query,
socket.assigns.sort_field,
socket.assigns.sort_order,
socket.assigns.cycle_status_filter,
socket.assigns[:group_filters],
new_show_current,
socket.assigns.boolean_custom_field_filters,
socket.assigns[:fee_type_filters]
)
|> maybe_add_field_selection( |> maybe_add_field_selection(
socket.assigns[:user_field_selection], socket.assigns[:user_field_selection],
socket.assigns[:fields_in_url?] || false socket.assigns[:fields_in_url?] || false
@ -306,14 +298,10 @@ defmodule MvWeb.MemberLive.Index do
# URL sync - push_patch happens synchronously in the event handler # URL sync - push_patch happens synchronously in the event handler
query_params = query_params =
build_query_params( build_query_params(
socket.assigns.query, opts_for_query_params(socket, %{
export_sort_field(socket.assigns.sort_field), sort_field: export_sort_field(socket.assigns.sort_field),
export_sort_order(socket.assigns.sort_order), sort_order: export_sort_order(socket.assigns.sort_order)
socket.assigns.cycle_status_filter, })
socket.assigns[:group_filters],
socket.assigns.show_current_cycle,
socket.assigns.boolean_custom_field_filters,
socket.assigns[:fee_type_filters]
) )
|> maybe_add_field_selection( |> maybe_add_field_selection(
socket.assigns[:user_field_selection], socket.assigns[:user_field_selection],
@ -345,16 +333,7 @@ defmodule MvWeb.MemberLive.Index do
|> update_selection_assigns() |> update_selection_assigns()
query_params = query_params =
build_query_params( build_query_params(opts_for_query_params(socket, %{query: q}))
q,
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,
socket.assigns[:fee_type_filters]
)
|> maybe_add_field_selection( |> maybe_add_field_selection(
socket.assigns[:user_field_selection], socket.assigns[:user_field_selection],
socket.assigns[:fields_in_url?] || false socket.assigns[:fields_in_url?] || false
@ -374,16 +353,7 @@ defmodule MvWeb.MemberLive.Index do
|> update_selection_assigns() |> update_selection_assigns()
query_params = query_params =
build_query_params( build_query_params(opts_for_query_params(socket, %{cycle_status_filter: filter}))
socket.assigns.query,
socket.assigns.sort_field,
socket.assigns.sort_order,
filter,
socket.assigns[:group_filters],
socket.assigns.show_current_cycle,
socket.assigns.boolean_custom_field_filters,
socket.assigns[:fee_type_filters]
)
|> maybe_add_field_selection( |> maybe_add_field_selection(
socket.assigns[:user_field_selection], socket.assigns[:user_field_selection],
socket.assigns[:fields_in_url?] || false socket.assigns[:fields_in_url?] || false
@ -409,16 +379,7 @@ defmodule MvWeb.MemberLive.Index do
|> update_selection_assigns() |> update_selection_assigns()
query_params = query_params =
build_query_params( build_query_params(opts_for_query_params(socket, %{boolean_filters: updated_filters}))
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,
updated_filters,
socket.assigns[:fee_type_filters]
)
|> maybe_add_field_selection( |> maybe_add_field_selection(
socket.assigns[:user_field_selection], socket.assigns[:user_field_selection],
socket.assigns[:fields_in_url?] || false socket.assigns[:fields_in_url?] || false
@ -446,16 +407,7 @@ defmodule MvWeb.MemberLive.Index do
|> update_selection_assigns() |> update_selection_assigns()
query_params = query_params =
build_query_params( build_query_params(opts_for_query_params(socket, %{group_filters: group_filters}))
socket.assigns.query,
socket.assigns.sort_field,
socket.assigns.sort_order,
socket.assigns.cycle_status_filter,
group_filters,
socket.assigns.show_current_cycle,
socket.assigns.boolean_custom_field_filters,
socket.assigns[:fee_type_filters]
)
|> maybe_add_field_selection( |> maybe_add_field_selection(
socket.assigns[:user_field_selection], socket.assigns[:user_field_selection],
socket.assigns[:fields_in_url?] || false socket.assigns[:fields_in_url?] || false
@ -483,16 +435,7 @@ defmodule MvWeb.MemberLive.Index do
|> update_selection_assigns() |> update_selection_assigns()
query_params = query_params =
build_query_params( build_query_params(opts_for_query_params(socket, %{fee_type_filters: fee_type_filters}))
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( |> maybe_add_field_selection(
socket.assigns[:user_field_selection], socket.assigns[:user_field_selection],
socket.assigns[:fields_in_url?] || false socket.assigns[:fields_in_url?] || false
@ -502,9 +445,18 @@ defmodule MvWeb.MemberLive.Index do
{:noreply, push_patch(socket, to: new_path, replace: true)} {:noreply, push_patch(socket, to: new_path, replace: true)}
end end
@impl true # Backward compatibility: tuple form delegates to map form
def handle_info({:reset_all_filters, cycle_status_filter, boolean_filters}, socket) do 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: cycle_status_filter,
boolean_filters: boolean_filters,
group_filters: %{},
fee_type_filters: %{}
}},
socket
)
end end
def handle_info( def handle_info(
@ -512,7 +464,13 @@ defmodule MvWeb.MemberLive.Index do
socket socket
) do ) do
handle_info( handle_info(
{:reset_all_filters, cycle_status_filter, boolean_filters, group_filters, %{}}, {:reset_all_filters,
%{
cycle_status_filter: cycle_status_filter,
boolean_filters: boolean_filters,
group_filters: group_filters,
fee_type_filters: %{}
}},
socket socket
) )
end end
@ -522,26 +480,30 @@ defmodule MvWeb.MemberLive.Index do
fee_type_filters}, fee_type_filters},
socket socket
) do ) do
handle_info(
{:reset_all_filters,
%{
cycle_status_filter: cycle_status_filter,
boolean_filters: boolean_filters,
group_filters: group_filters,
fee_type_filters: fee_type_filters
}},
socket
)
end
def handle_info({:reset_all_filters, %{} = opts}, socket) do
socket = socket =
socket socket
|> assign(:cycle_status_filter, cycle_status_filter) |> assign(:cycle_status_filter, Map.get(opts, :cycle_status_filter))
|> assign(:group_filters, group_filters) |> assign(:group_filters, Map.get(opts, :group_filters, %{}))
|> assign(:fee_type_filters, fee_type_filters) |> assign(:fee_type_filters, Map.get(opts, :fee_type_filters, %{}))
|> assign(:boolean_custom_field_filters, boolean_filters) |> assign(:boolean_custom_field_filters, Map.get(opts, :boolean_filters, %{}))
|> load_members() |> load_members()
|> update_selection_assigns() |> update_selection_assigns()
query_params = query_params =
build_query_params( build_query_params(opts_for_query_params(socket))
socket.assigns.query,
socket.assigns.sort_field,
socket.assigns.sort_order,
cycle_status_filter,
socket.assigns[:group_filters],
socket.assigns.show_current_cycle,
boolean_filters,
fee_type_filters
)
|> maybe_add_field_selection( |> maybe_add_field_selection(
socket.assigns[:user_field_selection], socket.assigns[:user_field_selection],
socket.assigns[:fields_in_url?] || false socket.assigns[:fields_in_url?] || false
@ -801,16 +763,7 @@ defmodule MvWeb.MemberLive.Index do
defp push_field_selection_url(socket) do defp push_field_selection_url(socket) do
query_params = query_params =
build_query_params( build_query_params(opts_for_query_params(socket))
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,
socket.assigns[:fee_type_filters]
)
|> maybe_add_field_selection(socket.assigns[:user_field_selection], true) |> maybe_add_field_selection(socket.assigns[:user_field_selection], true)
new_path = ~p"/members?#{query_params}" new_path = ~p"/members?#{query_params}"
@ -821,22 +774,27 @@ defmodule MvWeb.MemberLive.Index do
assign(socket, :user_field_selection, selection) assign(socket, :user_field_selection, selection)
end end
defp build_query_params( defp build_query_params(opts) when is_map(opts) do
query, base_params = build_base_params(opts.query, opts.sort_field, opts.sort_order)
sort_field, base_params = add_cycle_status_filter(base_params, opts.cycle_status_filter)
sort_order, base_params = add_group_filters(base_params, opts.group_filters || %{})
cycle_status_filter, base_params = add_fee_type_filters(base_params, opts.fee_type_filters || %{})
group_filters, base_params = add_show_current_cycle(base_params, opts.show_current_cycle)
show_current_cycle, add_boolean_filters(base_params, opts.boolean_filters || %{})
boolean_filters, end
fee_type_filters
) do defp opts_for_query_params(socket, overrides \\ %{}) do
base_params = build_base_params(query, sort_field, sort_order) %{
base_params = add_cycle_status_filter(base_params, cycle_status_filter) query: socket.assigns.query,
base_params = add_group_filters(base_params, group_filters) sort_field: socket.assigns.sort_field,
base_params = add_fee_type_filters(base_params, fee_type_filters || %{}) sort_order: socket.assigns.sort_order,
base_params = add_show_current_cycle(base_params, show_current_cycle) cycle_status_filter: socket.assigns.cycle_status_filter,
add_boolean_filters(base_params, boolean_filters) group_filters: socket.assigns[:group_filters] || %{},
show_current_cycle: socket.assigns.show_current_cycle,
boolean_filters: socket.assigns.boolean_custom_field_filters || %{},
fee_type_filters: socket.assigns[:fee_type_filters] || %{}
}
|> Map.merge(overrides)
end end
defp add_fee_type_filters(params, fee_type_filters) do defp add_fee_type_filters(params, fee_type_filters) do
@ -1146,7 +1104,8 @@ defmodule MvWeb.MemberLive.Index do
defp apply_one_group_filter(query, _, _), do: query defp apply_one_group_filter(query, _, _), do: query
# Multiple fee type filters combine with AND: member must match all selected fee type conditions. # Fee type filters: :in selections combine with OR (member has any of the selected types);
# :not_in selections combine with AND (member must not have type A and not have type B).
defp apply_fee_type_filters(query, fee_type_filters, _fee_types) when fee_type_filters == %{}, defp apply_fee_type_filters(query, fee_type_filters, _fee_types) when fee_type_filters == %{},
do: query do: query
@ -1157,27 +1116,28 @@ defmodule MvWeb.MemberLive.Index do
|> Enum.reject(&is_nil/1) |> Enum.reject(&is_nil/1)
|> MapSet.new() |> MapSet.new()
Enum.reduce(fee_type_filters, query, fn {fee_type_id_str, value}, q -> {in_id_strs, not_in_filters} =
member? = MapSet.member?(valid_ids, fee_type_id_str) fee_type_filters
|> Enum.filter(fn {id_str, _} -> MapSet.member?(valid_ids, id_str) end)
|> Enum.split_with(fn {_, value} -> value == :in end)
if member? do in_uuids =
apply_one_fee_type_filter(q, fee_type_id_str, value) in_id_strs
else |> Enum.map(fn {id_str, _} -> id_str end)
q |> Enum.map(&Ecto.UUID.cast/1)
end |> Enum.filter(&match?({:ok, _}, &1))
end) |> Enum.map(fn {:ok, uuid} -> uuid end)
end
defp apply_one_fee_type_filter(query, _fee_type_id_str, nil), do: query query =
if in_uuids == [] do
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 query
else
Ash.Query.filter(query, expr(membership_fee_type_id in ^in_uuids))
end end
Enum.reduce(not_in_filters, query, fn {fee_type_id_str, _}, q ->
apply_one_fee_type_filter(q, fee_type_id_str, :not_in)
end)
end end
defp apply_one_fee_type_filter(query, fee_type_id_str, :not_in) do defp apply_one_fee_type_filter(query, fee_type_id_str, :not_in) do
@ -1523,7 +1483,14 @@ defmodule MvWeb.MemberLive.Index do
add_group_filter_entry(acc, key, value_str, prefix_len) add_group_filter_entry(acc, key, value_str, prefix_len)
end) end)
assign(socket, :group_filters, filters) valid_group_ids =
socket.assigns.groups
|> Enum.map(&normalize_uuid_string(to_string(&1.id)))
|> Enum.reject(&is_nil/1)
|> MapSet.new()
|> MapSet.to_list()
assign(socket, :group_filters, Map.take(filters, valid_group_ids))
end end
defp maybe_update_group_filters(socket, _), do: socket defp maybe_update_group_filters(socket, _), do: socket
@ -1544,7 +1511,14 @@ defmodule MvWeb.MemberLive.Index do
add_fee_type_filter_entry(acc, key, value_str, prefix_len) add_fee_type_filter_entry(acc, key, value_str, prefix_len)
end) end)
assign(socket, :fee_type_filters, filters) valid_fee_type_ids =
socket.assigns.fee_types
|> Enum.map(&normalize_uuid_string(to_string(&1.id)))
|> Enum.reject(&is_nil/1)
|> MapSet.new()
|> MapSet.to_list()
assign(socket, :fee_type_filters, Map.take(filters, valid_fee_type_ids))
end end
defp maybe_update_fee_type_filters(socket, _), do: socket defp maybe_update_fee_type_filters(socket, _), do: socket
@ -1556,7 +1530,7 @@ defmodule MvWeb.MemberLive.Index do
valid_id? = fee_type_id_str && String.length(fee_type_id_str) <= @max_uuid_length valid_id? = fee_type_id_str && String.length(fee_type_id_str) <= @max_uuid_length
if valid_id? do if valid_id? do
case parse_fee_type_filter_value(value_str) do case FilterParams.parse_in_not_in_value(value_str) do
nil -> acc nil -> acc
value -> Map.put(acc, fee_type_id_str, value) value -> Map.put(acc, fee_type_id_str, value)
end end
@ -1565,15 +1539,6 @@ defmodule MvWeb.MemberLive.Index do
end end
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 defp add_group_filter_entry(acc, key, value_str, prefix_len) do
key_str = to_string(key) key_str = to_string(key)
raw_id = String.slice(key_str, prefix_len, String.length(key_str) - prefix_len) raw_id = String.slice(key_str, prefix_len, String.length(key_str) - prefix_len)
@ -1581,7 +1546,7 @@ defmodule MvWeb.MemberLive.Index do
valid_id? = group_id_str && String.length(group_id_str) <= @max_uuid_length valid_id? = group_id_str && String.length(group_id_str) <= @max_uuid_length
if valid_id? do if valid_id? do
case parse_group_filter_value(value_str) do case FilterParams.parse_in_not_in_value(value_str) do
nil -> acc nil -> acc
value -> Map.put(acc, group_id_str, value) value -> Map.put(acc, group_id_str, value)
end end
@ -1600,15 +1565,6 @@ defmodule MvWeb.MemberLive.Index do
defp normalize_uuid_string(_), do: nil defp normalize_uuid_string(_), do: nil
defp parse_group_filter_value("in"), do: :in
defp parse_group_filter_value("not_in"), do: :not_in
defp parse_group_filter_value(val) when is_binary(val) do
parse_group_filter_value(String.trim(val))
end
defp parse_group_filter_value(_), do: nil
defp determine_cycle_status_filter("paid"), do: :paid defp determine_cycle_status_filter("paid"), do: :paid
defp determine_cycle_status_filter("unpaid"), do: :unpaid defp determine_cycle_status_filter("unpaid"), do: :unpaid
defp determine_cycle_status_filter(_), do: nil defp determine_cycle_status_filter(_), do: nil

View file

@ -3234,6 +3234,18 @@ msgid "Include joining cycle: When active, members pay from their joining cycle;
msgstr "Beitrittszyklus einbeziehen: Aktiv = Zahlung ab Beitrittszyklus; inaktiv = ab dem nächsten vollen Zyklus." msgstr "Beitrittszyklus einbeziehen: Aktiv = Zahlung ab Beitrittszyklus; inaktiv = ab dem nächsten vollen Zyklus."
#: lib/mv_web/live/components/member_filter_component.ex #: lib/mv_web/live/components/member_filter_component.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format
msgid "Fee types" msgid "Fee types"
msgstr "Beitragsart" msgstr "Beitragsarten"
#: lib/mv_web/live/components/member_filter_component.ex
#, elixir-autogen, elixir-format
msgid "%{count} filter active"
msgid_plural "%{count} filters active"
msgstr[0] "%{count} Filter aktiv"
msgstr[1] "%{count} Filter aktiv"
#: lib/mv_web/live/components/member_filter_component.ex
#, elixir-autogen, elixir-format
msgid "without %{name}"
msgstr "ohne %{name}"

View file

@ -3237,3 +3237,15 @@ msgstr ""
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Fee types" msgid "Fee types"
msgstr "" msgstr ""
#: lib/mv_web/live/components/member_filter_component.ex
#, elixir-autogen, elixir-format
msgid "%{count} filter active"
msgid_plural "%{count} filters active"
msgstr[0] ""
msgstr[1] ""
#: lib/mv_web/live/components/member_filter_component.ex
#, elixir-autogen, elixir-format
msgid "without %{name}"
msgstr ""

View file

@ -3234,6 +3234,18 @@ msgid "Include joining cycle: When active, members pay from their joining cycle;
msgstr "Include joining cycle: When active, members pay from their joining cycle; when inactive, from the next full cycle." msgstr "Include joining cycle: When active, members pay from their joining cycle; when inactive, from the next full cycle."
#: lib/mv_web/live/components/member_filter_component.ex #: lib/mv_web/live/components/member_filter_component.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format
msgid "Fee types" msgid "Fee types"
msgstr "" msgstr "Fee types"
#: lib/mv_web/live/components/member_filter_component.ex
#, elixir-autogen, elixir-format
msgid "%{count} filter active"
msgid_plural "%{count} filters active"
msgstr[0] "%{count} filter active"
msgstr[1] "%{count} filters active"
#: lib/mv_web/live/components/member_filter_component.ex
#, elixir-autogen, elixir-format
msgid "without %{name}"
msgstr "without %{name}"