diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index dc15ba0..53a5705 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -32,6 +32,7 @@ defmodule MvWeb.MemberLive.Index do import MvWeb.LiveHelpers, only: [current_actor: 1] alias Mv.Membership + alias Mv.Membership.CustomFieldSort alias Mv.Membership.Member, as: MemberResource alias Mv.MembershipFees alias Mv.MembershipFees.MembershipFeeType @@ -1414,8 +1415,7 @@ defmodule MvWeb.MemberLive.Index do false cfv -> - extracted = extract_sort_value(cfv.value, custom_field.value_type) - not empty_value?(extracted, custom_field.value_type) + not empty_value?(cfv.value, custom_field.value_type) end end @@ -1423,29 +1423,22 @@ defmodule MvWeb.MemberLive.Index do sorted = Enum.sort_by(members_with_values, fn member -> cfv = get_custom_field_value(member, custom_field) - extracted = extract_sort_value(cfv.value, custom_field.value_type) - normalize_sort_value(extracted, order) + CustomFieldSort.sort_key(cfv.value, custom_field.value_type) end) if order == :desc, do: Enum.reverse(sorted), else: sorted end - defp extract_sort_value(%Ash.Union{value: value, type: type}, _expected_type), - do: extract_sort_value(value, type) + defp empty_value?(%Ash.Union{value: value, type: type}, _expected_type), + do: empty_value?(value, type) - defp extract_sort_value(value, :string) when is_binary(value), do: value - defp extract_sort_value(value, :integer) when is_integer(value), do: value - defp extract_sort_value(value, :boolean) when is_boolean(value), do: value - defp extract_sort_value(%Date{} = date, :date), do: date - defp extract_sort_value(value, :email) when is_binary(value), do: value - defp extract_sort_value(value, _type), do: to_string(value) + defp empty_value?(nil, _type), do: true + + defp empty_value?(value, type) when type in [:string, :email] and is_binary(value), + do: String.trim(value) == "" - defp empty_value?(value, :string) when is_binary(value), do: String.trim(value) == "" - defp empty_value?(value, :email) when is_binary(value), do: String.trim(value) == "" defp empty_value?(_value, _type), do: false - defp normalize_sort_value(value, _order), do: value - defp maybe_update_sort(socket, %{"sort_field" => sf, "sort_order" => so}) do field = determine_field(socket.assigns.sort_field, sf) order = determine_order(socket.assigns.sort_order, so) diff --git a/test/mv_web/member_live/index_custom_fields_sorting_test.exs b/test/mv_web/member_live/index_custom_fields_sorting_test.exs index 2f12fcc..afce67b 100644 --- a/test/mv_web/member_live/index_custom_fields_sorting_test.exs +++ b/test/mv_web/member_live/index_custom_fields_sorting_test.exs @@ -231,6 +231,63 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do assert has_element?(view, "[data-testid='custom_field_#{field.id}'][aria-label='ascending']") end + test "sorts members chronologically by a :date custom field (ascending)", %{conn: conn} do + system_actor = Mv.Helpers.SystemActor.get_system_actor() + + # Dates chosen to expose the day-first term-ordering trap: term order of the + # %Date{} structs compares day, then month, then year, which would place + # 02.07.1986 (day 02) before 29.01.1981 (day 29). Chronologically 1981 < 1982 < 1986. + members_and_dates = [ + {"EightySix", ~D[1986-07-02]}, + {"EightyOne", ~D[1981-01-29]}, + {"EightyTwo", ~D[1982-03-01]} + ] + + {:ok, field} = + CustomField + |> Ash.Changeset.for_create(:create, %{ + name: "birth_date", + value_type: :date, + show_in_overview: true + }) + |> Ash.create(actor: system_actor) + + for {first_name, date} <- members_and_dates do + {:ok, member} = + Mv.Membership.create_member( + %{ + first_name: first_name, + last_name: "Test", + email: "#{String.downcase(first_name)}@example.com" + }, + actor: system_actor + ) + + {:ok, _cfv} = + CustomFieldValue + |> Ash.Changeset.for_create(:create, %{ + member_id: member.id, + custom_field_id: field.id, + value: %{"_union_type" => "date", "_union_value" => Date.to_iso8601(date)} + }) + |> Ash.create(actor: system_actor) + end + + conn = conn_with_oidc_user(conn) + + {:ok, view, _html} = + live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=asc") + + html = render(view) + + {one_idx, _} = :binary.match(html, "EightyOne") + {two_idx, _} = :binary.match(html, "EightyTwo") + {six_idx, _} = :binary.match(html, "EightySix") + + assert one_idx < two_idx, "29.01.1981 must come before 01.03.1982 in ascending order" + assert two_idx < six_idx, "01.03.1982 must come before 02.07.1986 in ascending order" + end + test "NULL values and empty strings are always sorted last (ASC)", %{conn: conn} do system_actor = Mv.Helpers.SystemActor.get_system_actor()