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.
This commit is contained in:
Moritz 2026-02-23 23:53:50 +01:00
parent b7ef69813b
commit 68ceaced0c
Signed by: moritz
GPG key ID: 1020A035E5DD0824
2 changed files with 89 additions and 22 deletions

View file

@ -913,6 +913,14 @@ defmodule MvWeb.MemberLive.Index do
query = query =
Ash.Query.load(query, groups: [:id, :name, :slug]) 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_search_filter(query, search_query)
query = apply_group_filters(query, socket.assigns[:group_filters], socket.assigns[:groups]) 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"] -> field in [:groups, "groups"] ->
{query, true} {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 -> after load
custom_field_sort?(field) -> custom_field_sort?(field) ->
{query, true} {query, true}
@ -1118,11 +1130,16 @@ defmodule MvWeb.MemberLive.Index do
defp valid_sort_field_db_or_custom?(field) when is_atom(field) do defp valid_sort_field_db_or_custom?(field) when is_atom(field) do
non_sortable_fields = [:notes] non_sortable_fields = [:notes]
valid_fields = Mv.Constants.member_fields() -- non_sortable_fields 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 end
defp valid_sort_field_db_or_custom?(field) when is_binary(field) do 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 (normalized != nil and valid_sort_field_db_or_custom?(normalized)) or
custom_field_sort?(field) custom_field_sort?(field)
@ -1647,13 +1664,11 @@ defmodule MvWeb.MemberLive.Index do
FieldVisibility.computed_member_fields() FieldVisibility.computed_member_fields()
|> Enum.filter(&(&1 in member_fields_computed)) |> Enum.filter(&(&1 in member_fields_computed))
# Include groups in export only if it's visible in the table
member_fields_with_groups = member_fields_with_groups =
if :groups in socket.assigns[:member_fields_visible] do build_export_member_fields_list(
ordered_member_fields_db ++ ["groups"] ordered_member_fields_db,
else socket.assigns[:member_fields_visible]
ordered_member_fields_db )
end
# Order custom fields like the table (same as dynamic_cols / all_custom_fields order) # Order custom fields like the table (same as dynamic_cols / all_custom_fields order)
ordered_custom_field_ids = ordered_custom_field_ids =
@ -1674,7 +1689,9 @@ defmodule MvWeb.MemberLive.Index do
export_column_order( export_column_order(
ordered_member_fields_db, ordered_member_fields_db,
ordered_computed_fields, 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, query: socket.assigns[:query] || nil,
sort_field: export_sort_field(socket.assigns[:sort_field]), sort_field: export_sort_field(socket.assigns[:sort_field]),
@ -1685,6 +1702,32 @@ defmodule MvWeb.MemberLive.Index do
} }
end 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(nil), do: nil
defp export_cycle_status_filter(:paid), do: "paid" defp export_cycle_status_filter(:paid), do: "paid"
defp export_cycle_status_filter(:unpaid), do: "unpaid" 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 defp export_sort_order(o) when is_binary(o), do: o
# Build a single ordered list that matches the table order: # 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) # - 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) # - custom fields appended in the same order as table (already ordered_custom_field_ids)
defp export_column_order( defp export_column_order(
ordered_member_fields_db, ordered_member_fields_db,
ordered_computed_fields, ordered_computed_fields,
ordered_custom_field_ids ordered_custom_field_ids,
membership_fee_type_visible,
groups_visible
) do ) do
db_strings = Enum.map(ordered_member_fields_db, &Atom.to_string/1) db_strings = Enum.map(ordered_member_fields_db, &Atom.to_string/1)
computed_strings = Enum.map(ordered_computed_fields, &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 # Place membership_fee_type and membership_fee_status after membership_fee_start_date when present
db_with_computed = db_with_extras =
Enum.flat_map(db_strings, fn f -> Enum.flat_map(
if f == "membership_fee_start_date" and "membership_fee_status" in computed_strings do db_strings,
[f, "membership_fee_status"] &expand_db_string_for_export(&1, membership_fee_type_visible, computed_strings)
else )
[f]
end
end)
# Any remaining computed fields not inserted above (future-proof) # Any remaining computed fields not inserted above (future-proof)
remaining_computed = remaining_computed =
computed_strings 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
end end

View file

@ -313,6 +313,28 @@
> >
{MvWeb.MemberLive.Index.format_date(member.membership_fee_start_date)} {MvWeb.MemberLive.Index.format_date(member.membership_fee_start_date)}
</:col> </:col>
<: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 %>
<span class="text-base-content/50">—</span>
<% end %>
</:col>
<:col <:col
:let={member} :let={member}
:if={:membership_fee_status in @member_fields_visible} :if={:membership_fee_status in @member_fields_visible}