Merge branch 'main' into feat/299_plz
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
carla 2026-02-24 11:44:19 +01:00
commit 9a7608f9a1
16 changed files with 573 additions and 88 deletions

View file

@ -16,7 +16,7 @@ defmodule Mv.Membership.MemberExport do
alias MvWeb.MemberLive.Index.MembershipFeeStatus
@member_fields_allowlist (Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)) ++
["membership_fee_status", "groups"]
["membership_fee_type", "membership_fee_status", "groups"]
@computed_export_fields ["membership_fee_status"]
@computed_insert_after "membership_fee_start_date"
@custom_field_prefix Mv.Constants.custom_field_prefix()
@ -326,10 +326,10 @@ defmodule Mv.Membership.MemberExport do
# Separate groups from other fields (groups is handled as a special field, not a member field)
groups_field = if "groups" in member_fields, do: ["groups"], else: []
# final member_fields list (used for column specs order): table order + computed inserted + groups
# final member_fields list (used for column specs order): table order + fee type + computed + groups
ordered_member_fields =
selectable_member_fields
|> insert_computed_fields_like_table(computed_fields)
|> insert_fee_type_and_computed_fields_like_table(computed_fields, member_fields)
|> then(fn fields -> fields ++ groups_field end)
%{
@ -420,27 +420,52 @@ defmodule Mv.Membership.MemberExport do
table_order |> Enum.filter(&(&1 in fields))
end
defp insert_computed_fields_like_table(db_fields_ordered, computed_fields) do
# Insert membership_fee_status right after membership_fee_start_date (if both selected),
# otherwise append at the end of DB fields.
defp insert_fee_type_and_computed_fields_like_table(
db_fields_ordered,
computed_fields,
member_fields
) do
computed_fields = computed_fields || []
member_fields = member_fields || []
db_with_insert =
Enum.flat_map(db_fields_ordered, fn f ->
if f == @computed_insert_after and "membership_fee_status" in computed_fields do
[f, "membership_fee_status"]
else
[f]
end
expand_field_with_computed(f, member_fields, computed_fields)
end)
remaining =
computed_fields
|> Enum.reject(&(&1 in db_with_insert))
# If fee type is visible but start_date was not in the list, it won't be in db_with_insert
db_with_insert =
if "membership_fee_type" in member_fields and "membership_fee_type" not in db_with_insert do
db_with_insert ++ ["membership_fee_type"]
else
db_with_insert
end
remaining = Enum.reject(computed_fields, &(&1 in db_with_insert))
db_with_insert ++ remaining
end
# Insert membership_fee_type and membership_fee_status after membership_fee_start_date (table order).
defp expand_field_with_computed(f, member_fields, computed_fields) do
if f == @computed_insert_after do
extra = []
extra =
if "membership_fee_type" in member_fields,
do: extra ++ ["membership_fee_type"],
else: extra
extra =
if "membership_fee_status" in computed_fields,
do: extra ++ ["membership_fee_status"],
else: extra
[f] ++ extra
else
[f]
end
end
defp normalize_computed_fields(fields) when is_list(fields) do
fields
|> Enum.filter(&is_binary/1)

View file

@ -134,6 +134,10 @@ defmodule Mv.Membership.MemberExport.Build do
need_groups = "groups" in parsed.member_fields
need_membership_fee_type =
"membership_fee_type" in parsed.member_fields or
parsed.sort_field == "membership_fee_type"
query =
Member
|> Ash.Query.new()
@ -141,6 +145,7 @@ defmodule Mv.Membership.MemberExport.Build do
|> load_custom_field_values_query(custom_field_ids_union)
|> maybe_load_cycles(need_cycles, parsed.show_current_cycle)
|> maybe_load_groups(need_groups)
|> maybe_load_membership_fee_type(need_membership_fee_type)
query =
if parsed.selected_ids != [] do
@ -196,8 +201,10 @@ defmodule Mv.Membership.MemberExport.Build do
defp sort_members_in_memory(members, field, order) when is_binary(field) do
field_atom = String.to_existing_atom(field)
if field_atom in Mv.Constants.member_fields() do
sort_by_field(members, field_atom, order)
if field_atom in Mv.Constants.member_fields() or field_atom == :membership_fee_type do
key_fn = sort_key_fn_for_field(field_atom)
compare_fn = build_compare_fn(order)
Enum.sort_by(members, key_fn, compare_fn)
else
members
end
@ -207,13 +214,17 @@ defmodule Mv.Membership.MemberExport.Build do
defp sort_members_in_memory(members, _field, _order), do: members
defp sort_by_field(members, field_atom, order) do
key_fn = fn member -> Map.get(member, field_atom) end
compare_fn = build_compare_fn(order)
Enum.sort_by(members, key_fn, compare_fn)
defp sort_key_fn_for_field(:membership_fee_type) do
fn member ->
case Map.get(member, :membership_fee_type) do
nil -> nil
rel -> Map.get(rel, :name)
end
end
end
defp sort_key_fn_for_field(field_atom), do: fn member -> Map.get(member, field_atom) end
defp build_compare_fn("asc"), do: fn a, b -> a <= b end
defp build_compare_fn("desc"), do: fn a, b -> b <= a end
defp build_compare_fn(_), do: fn _a, _b -> true end
@ -245,26 +256,41 @@ defmodule Mv.Membership.MemberExport.Build do
defp maybe_sort(query, field, order) when is_binary(field) do
cond do
field == "groups" ->
# Groups sort → in-memory nach dem Read (wie Tabelle)
{query, true}
custom_field_sort?(field) ->
{query, true}
true ->
field_atom = String.to_existing_atom(field)
if field_atom in (Mv.Constants.member_fields() -- [:notes]) do
{Ash.Query.sort(query, [{field_atom, String.to_existing_atom(order)}]), false}
else
{query, false}
end
field == "groups" -> {query, true}
field == "membership_fee_type" -> apply_fee_type_sort(query, order)
custom_field_sort?(field) -> {query, true}
true -> apply_standard_member_sort(query, field, order)
end
rescue
ArgumentError -> {query, false}
end
defp apply_fee_type_sort(query, order) do
order_atom = if order == "desc", do: :desc, else: :asc
{Ash.Query.sort(query, [{"membership_fee_type.name", order_atom}]), false}
end
defp apply_standard_member_sort(query, field, order) do
field_atom = String.to_existing_atom(field)
sortable =
field_atom in (Mv.Constants.member_fields() -- [:notes]) or
field_atom == :membership_fee_type
if sortable do
order_atom = if order == "desc", do: :desc, else: :asc
sort_field =
if field_atom == :membership_fee_type,
do: {"membership_fee_type.name", order_atom},
else: {field_atom, order_atom}
{Ash.Query.sort(query, [sort_field]), false}
else
{query, false}
end
end
defp sort_members_by_custom_field(members, _field, _order, _custom_fields) when members == [],
do: []
@ -344,6 +370,12 @@ defmodule Mv.Membership.MemberExport.Build do
Ash.Query.load(query, groups: [:id, :name])
end
defp maybe_load_membership_fee_type(query, false), do: query
defp maybe_load_membership_fee_type(query, true) do
Ash.Query.load(query, membership_fee_type: [:id, :name])
end
defp apply_cycle_status_filter(members, nil, _show_current), do: members
defp apply_cycle_status_filter(members, status, show_current) when status in [:paid, :unpaid] do
@ -393,6 +425,19 @@ defmodule Mv.Membership.MemberExport.Build do
}
end)
membership_fee_type_col =
if "membership_fee_type" in parsed.member_fields do
[
%{
key: :membership_fee_type,
kind: :membership_fee_type,
label: label_fn.(:membership_fee_type)
}
]
else
[]
end
groups_col =
if "groups" in parsed.member_fields do
[
@ -424,7 +469,8 @@ defmodule Mv.Membership.MemberExport.Build do
end)
|> Enum.reject(&is_nil/1)
member_cols ++ computed_cols ++ groups_col ++ custom_cols
# Table order: ... membership_fee_start_date, membership_fee_type, membership_fee_status, groups, custom
member_cols ++ membership_fee_type_col ++ computed_cols ++ groups_col ++ custom_cols
end
defp build_rows(members, columns, custom_fields_by_id) do
@ -454,6 +500,17 @@ defmodule Mv.Membership.MemberExport.Build do
if is_binary(value), do: value, else: ""
end
defp cell_value(
member,
%{kind: :membership_fee_type, key: :membership_fee_type},
_custom_fields_by_id
) do
case Map.get(member, :membership_fee_type) do
%{name: name} when is_binary(name) -> name
_ -> ""
end
end
defp cell_value(member, %{kind: :groups, key: :groups}, _custom_fields_by_id) do
groups = Map.get(member, :groups) || []
format_groups(groups)

View file

@ -59,6 +59,13 @@ defmodule Mv.Membership.MembersCSV do
if is_binary(value), do: value, else: ""
end
defp cell_value(member, %{kind: :membership_fee_type, key: :membership_fee_type}) do
case Map.get(member, :membership_fee_type) do
%{name: name} when is_binary(name) -> name
_ -> ""
end
end
defp cell_value(member, %{kind: :groups, key: :groups}) do
groups = Map.get(member, :groups) || []
format_groups(groups)