diff --git a/lib/mv/membership/member_export/build.ex b/lib/mv/membership/member_export/build.ex index 323681d..ff8cf76 100644 --- a/lib/mv/membership/member_export/build.ex +++ b/lib/mv/membership/member_export/build.ex @@ -244,16 +244,22 @@ defmodule Mv.Membership.MemberExport.Build do defp maybe_sort(query, _field, nil), do: {query, false} defp maybe_sort(query, field, order) when is_binary(field) do - if custom_field_sort?(field) do - {query, true} - else - field_atom = String.to_existing_atom(field) + cond do + field == "groups" -> + # Groups sort → in-memory nach dem Read (wie Tabelle) + {query, true} - 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 + 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 end rescue ArgumentError -> {query, false} @@ -263,21 +269,45 @@ defmodule Mv.Membership.MemberExport.Build do do: [] defp sort_members_by_custom_field(members, field, order, custom_fields) when is_binary(field) do - id_str = String.trim_leading(field, @custom_field_prefix) - custom_field = Enum.find(custom_fields, fn cf -> to_string(cf.id) == id_str end) + if field == "groups" do + sort_members_by_groups_export(members, order) + else + id_str = String.trim_leading(field, @custom_field_prefix) + custom_field = Enum.find(custom_fields, fn cf -> to_string(cf.id) == id_str end) - if is_nil(custom_field), do: members + if is_nil(custom_field), do: members - key_fn = fn member -> - cfv = find_cfv(member, custom_field) - raw = if cfv, do: cfv.value, else: nil - MemberExportSort.custom_field_sort_key(custom_field.value_type, raw) + key_fn = fn member -> + cfv = find_cfv(member, custom_field) + raw = if cfv, do: cfv.value, else: nil + MemberExportSort.custom_field_sort_key(custom_field.value_type, raw) + end + + members + |> Enum.map(fn m -> {m, key_fn.(m)} end) + |> Enum.sort(fn {_, ka}, {_, kb} -> MemberExportSort.key_lt(ka, kb, order) end) + |> Enum.map(fn {m, _} -> m end) + end + end + + defp sort_members_by_groups_export(members, order) do + # Members with groups first, then by first group name alphabetically (min = first by sort order) + # Match table behavior from MvWeb.MemberLive.Index.sort_members_by_groups/2 + first_group_name = fn member -> + (member.groups || []) + |> Enum.map(& &1.name) + |> Enum.min(fn -> nil end) end members - |> Enum.map(fn m -> {m, key_fn.(m)} end) - |> Enum.sort(fn {_, ka}, {_, kb} -> MemberExportSort.key_lt(ka, kb, order) end) - |> Enum.map(fn {m, _} -> m end) + |> Enum.sort_by(fn member -> + name = first_group_name.(member) + # Nil (no groups) sorts last in asc, first in desc + {name == nil, name || ""} + end) + |> then(fn list -> + if order == "desc", do: Enum.reverse(list), else: list + end) end defp find_cfv(member, custom_field) do diff --git a/lib/mv_web/controllers/member_export_controller.ex b/lib/mv_web/controllers/member_export_controller.ex index 911e6d9..4ed8f2d 100644 --- a/lib/mv_web/controllers/member_export_controller.ex +++ b/lib/mv_web/controllers/member_export_controller.ex @@ -341,17 +341,23 @@ defmodule MvWeb.MemberExportController do defp maybe_sort_export(query, _field, nil), do: {query, false} defp maybe_sort_export(query, field, order) when is_binary(field) do - if custom_field_sort?(field) do - # Custom field sort → in-memory nach dem Read (wie Tabelle) - {query, true} - else - field_atom = String.to_existing_atom(field) + cond do + field == "groups" -> + # Groups sort → in-memory nach dem Read (wie Tabelle) + {query, true} - 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 + custom_field_sort?(field) -> + # Custom field sort → in-memory nach dem Read (wie Tabelle) + {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 end rescue ArgumentError -> {query, false} @@ -370,35 +376,60 @@ defmodule MvWeb.MemberExportController do defp sort_members_by_custom_field_export(members, field, order, custom_fields) when is_binary(field) do order = order || "asc" - id_str = String.trim_leading(field, @custom_field_prefix) - custom_field = - Enum.find(custom_fields, fn cf -> to_string(cf.id) == id_str end) - - if is_nil(custom_field) do - members + if field == "groups" do + sort_members_by_groups_export(members, order) else - # Match table: - # 1) values first, empty last - # 2) sort only values - # 3) for desc, reverse only the values-part - {with_values, without_values} = - Enum.split_with(members, fn member -> - has_non_empty_custom_field_value?(member, custom_field) - end) + id_str = String.trim_leading(field, @custom_field_prefix) - sorted_with_values = - Enum.sort_by(with_values, fn member -> - extract_member_sort_value(member, custom_field) - end) + custom_field = + Enum.find(custom_fields, fn cf -> to_string(cf.id) == id_str end) - sorted_with_values = - if order == "desc", do: Enum.reverse(sorted_with_values), else: sorted_with_values + if is_nil(custom_field) do + members + else + # Match table: + # 1) values first, empty last + # 2) sort only values + # 3) for desc, reverse only the values-part + {with_values, without_values} = + Enum.split_with(members, fn member -> + has_non_empty_custom_field_value?(member, custom_field) + end) - sorted_with_values ++ without_values + sorted_with_values = + Enum.sort_by(with_values, fn member -> + extract_member_sort_value(member, custom_field) + end) + + sorted_with_values = + if order == "desc", do: Enum.reverse(sorted_with_values), else: sorted_with_values + + sorted_with_values ++ without_values + end end end + defp sort_members_by_groups_export(members, order) do + # Members with groups first, then by first group name alphabetically (min = first by sort order) + # Match table behavior from MvWeb.MemberLive.Index.sort_members_by_groups/2 + first_group_name = fn member -> + (member.groups || []) + |> Enum.map(& &1.name) + |> Enum.min(fn -> nil end) + end + + members + |> Enum.sort_by(fn member -> + name = first_group_name.(member) + # Nil (no groups) sorts last in asc, first in desc + {name == nil, name || ""} + end) + |> then(fn list -> + if order == "desc", do: Enum.reverse(list), else: list + end) + end + defp has_non_empty_custom_field_value?(member, custom_field) do case find_cfv(member, custom_field) do nil -> diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 526f858..9d93b08 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -1066,13 +1066,6 @@ defmodule MvWeb.MemberLive.Index do end end - defp computed_field?(field) do - computed_atoms = FieldVisibility.computed_member_fields() - computed_strings = Enum.map(computed_atoms, &Atom.to_string/1) - - (is_atom(field) and field in computed_atoms) or - (is_binary(field) and field in computed_strings) - end defp apply_sort_to_query(query, field, order) do cond do