From 68ceaced0cfbf7e73786cb4844eea329bf0d6a82 Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 23 Feb 2026 23:53:50 +0100 Subject: [PATCH] feat(members): show and sort by Fee Type in member overview Load membership_fee_type when column visible; sort by membership_fee_type_id; add table column with SortHeader and fee type name. --- lib/mv_web/live/member_live/index.ex | 89 ++++++++++++++++----- lib/mv_web/live/member_live/index.html.heex | 22 +++++ 2 files changed, 89 insertions(+), 22 deletions(-) diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 218fa6f..e7c0fd7 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -913,6 +913,14 @@ defmodule MvWeb.MemberLive.Index do query = Ash.Query.load(query, groups: [:id, :name, :slug]) + # Load membership_fee_type when the column is visible + query = + if :membership_fee_type in socket.assigns.member_fields_visible do + Ash.Query.load(query, membership_fee_type: [:id, :name]) + else + query + end + query = apply_search_filter(query, search_query) query = apply_group_filters(query, socket.assigns[:group_filters], socket.assigns[:groups]) @@ -1073,6 +1081,10 @@ defmodule MvWeb.MemberLive.Index do field in [:groups, "groups"] -> {query, true} + # Membership fee type sort -> by FK at DB + field in [:membership_fee_type, "membership_fee_type"] -> + {Ash.Query.sort(query, membership_fee_type_id: order), false} + # Custom field sort -> after load custom_field_sort?(field) -> {query, true} @@ -1118,11 +1130,16 @@ defmodule MvWeb.MemberLive.Index do defp valid_sort_field_db_or_custom?(field) when is_atom(field) do non_sortable_fields = [:notes] valid_fields = Mv.Constants.member_fields() -- non_sortable_fields - field in valid_fields or custom_field_sort?(field) or field == :groups + field in valid_fields or custom_field_sort?(field) or field in [:groups, :membership_fee_type] end defp valid_sort_field_db_or_custom?(field) when is_binary(field) do - normalized = if field == "groups", do: :groups, else: safe_member_field_atom_only(field) + normalized = + cond do + field == "groups" -> :groups + field == "membership_fee_type" -> :membership_fee_type + true -> safe_member_field_atom_only(field) + end (normalized != nil and valid_sort_field_db_or_custom?(normalized)) or custom_field_sort?(field) @@ -1647,13 +1664,11 @@ defmodule MvWeb.MemberLive.Index do FieldVisibility.computed_member_fields() |> Enum.filter(&(&1 in member_fields_computed)) - # Include groups in export only if it's visible in the table member_fields_with_groups = - if :groups in socket.assigns[:member_fields_visible] do - ordered_member_fields_db ++ ["groups"] - else - ordered_member_fields_db - end + build_export_member_fields_list( + ordered_member_fields_db, + socket.assigns[:member_fields_visible] + ) # Order custom fields like the table (same as dynamic_cols / all_custom_fields order) ordered_custom_field_ids = @@ -1674,7 +1689,9 @@ defmodule MvWeb.MemberLive.Index do export_column_order( ordered_member_fields_db, ordered_computed_fields, - ordered_custom_field_ids + ordered_custom_field_ids, + :membership_fee_type in socket.assigns[:member_fields_visible], + :groups in socket.assigns[:member_fields_visible] ), query: socket.assigns[:query] || nil, sort_field: export_sort_field(socket.assigns[:sort_field]), @@ -1685,6 +1702,32 @@ defmodule MvWeb.MemberLive.Index do } end + defp expand_db_string_for_export(f, membership_fee_type_visible, computed_strings) do + if f == "membership_fee_start_date" do + extra = + if(membership_fee_type_visible, do: ["membership_fee_type"], else: []) ++ + if "membership_fee_status" in computed_strings, do: ["membership_fee_status"], else: [] + + [f] ++ extra + else + [f] + end + end + + defp build_export_member_fields_list(ordered_db, member_fields_visible) do + with_extras = + Enum.flat_map(ordered_db, fn f -> + if f == :membership_fee_start_date and + :membership_fee_type in (member_fields_visible || []) do + [f, :membership_fee_type] + else + [f] + end + end) + + if :groups in (member_fields_visible || []), do: with_extras ++ [:groups], else: with_extras + end + defp export_cycle_status_filter(nil), do: nil defp export_cycle_status_filter(:paid), do: "paid" defp export_cycle_status_filter(:unpaid), do: "unpaid" @@ -1700,31 +1743,33 @@ defmodule MvWeb.MemberLive.Index do defp export_sort_order(o) when is_binary(o), do: o # Build a single ordered list that matches the table order: # - DB fields in Mv.Constants.member_fields() order (already pre-filtered as ordered_member_fields_db) - # - computed fields inserted at the correct position (membership_fee_status after membership_fee_start_date) + # - membership_fee_type and membership_fee_status inserted after membership_fee_start_date when visible + # - groups appended before custom fields when visible # - custom fields appended in the same order as table (already ordered_custom_field_ids) defp export_column_order( ordered_member_fields_db, ordered_computed_fields, - ordered_custom_field_ids + ordered_custom_field_ids, + membership_fee_type_visible, + groups_visible ) do db_strings = Enum.map(ordered_member_fields_db, &Atom.to_string/1) computed_strings = Enum.map(ordered_computed_fields, &Atom.to_string/1) - # Place membership_fee_status right after membership_fee_start_date if present in export - db_with_computed = - Enum.flat_map(db_strings, fn f -> - if f == "membership_fee_start_date" and "membership_fee_status" in computed_strings do - [f, "membership_fee_status"] - else - [f] - end - end) + # Place membership_fee_type and membership_fee_status after membership_fee_start_date when present + db_with_extras = + Enum.flat_map( + db_strings, + &expand_db_string_for_export(&1, membership_fee_type_visible, computed_strings) + ) # Any remaining computed fields not inserted above (future-proof) remaining_computed = computed_strings - |> Enum.reject(&(&1 in db_with_computed)) + |> Enum.reject(&(&1 in db_with_extras)) - db_with_computed ++ remaining_computed ++ ordered_custom_field_ids + result = db_with_extras ++ remaining_computed + result = if groups_visible, do: result ++ ["groups"], else: result + result ++ ordered_custom_field_ids end end diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index fa0f43a..fcf06c8 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -313,6 +313,28 @@ > {MvWeb.MemberLive.Index.format_date(member.membership_fee_start_date)} + <:col + :let={member} + :if={:membership_fee_type in @member_fields_visible} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_membership_fee_type} + field={:membership_fee_type} + label={gettext("Fee Type")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + <%= if member.membership_fee_type do %> + {member.membership_fee_type.name} + <% else %> + + <% end %> + <:col :let={member} :if={:membership_fee_status in @member_fields_visible}