refactor(member-live): modularize the overview and drop the superseded in-memory paths

Extract the cookie parser, export-payload builder and fee-status helper
into their own modules and remove the filter/sort/load helpers the
database pushdown made dead, so the overview LiveView stays a thin
coordinator over focused units.
This commit is contained in:
Simon 2026-07-06 10:54:09 +02:00
parent c2cb3edab8
commit 77fc11a0b0
17 changed files with 375 additions and 875 deletions

View file

@ -35,6 +35,7 @@ defmodule MvWeb.MemberLive.Index do
alias MvWeb.Helpers.DateFormatter
alias MvWeb.MemberLive.Index.CustomFieldValueLookup
alias MvWeb.MemberLive.Index.DateFilter
alias MvWeb.MemberLive.Index.ExportPayload
alias MvWeb.MemberLive.Index.FieldSelection
alias MvWeb.MemberLive.Index.FieldVisibility
alias MvWeb.MemberLive.Index.FilterParams
@ -377,8 +378,8 @@ defmodule MvWeb.MemberLive.Index do
query_params =
build_query_params(
opts_for_query_params(socket, %{
sort_field: export_sort_field(socket.assigns.sort_field),
sort_order: export_sort_order(socket.assigns.sort_order)
sort_field: ExportPayload.sort_field(socket.assigns.sort_field),
sort_order: ExportPayload.sort_order(socket.assigns.sort_order)
})
)
|> maybe_add_field_selection(
@ -553,53 +554,6 @@ defmodule MvWeb.MemberLive.Index do
{:noreply, push_reload(socket, new_path)}
end
# Backward compatibility: tuple form delegates to map form
def handle_info({:reset_all_filters, cycle_status_filter, boolean_filters}, socket) do
handle_info(
{:reset_all_filters,
%{
cycle_status_filter: cycle_status_filter,
boolean_filters: boolean_filters,
group_filters: %{},
fee_type_filters: %{}
}},
socket
)
end
def handle_info(
{:reset_all_filters, cycle_status_filter, boolean_filters, group_filters},
socket
) do
handle_info(
{:reset_all_filters,
%{
cycle_status_filter: cycle_status_filter,
boolean_filters: boolean_filters,
group_filters: group_filters,
fee_type_filters: %{}
}},
socket
)
end
def handle_info(
{:reset_all_filters, cycle_status_filter, boolean_filters, group_filters,
fee_type_filters},
socket
) do
handle_info(
{:reset_all_filters,
%{
cycle_status_filter: cycle_status_filter,
boolean_filters: boolean_filters,
group_filters: group_filters,
fee_type_filters: fee_type_filters
}},
socket
)
end
def handle_info({:reset_all_filters, %{} = opts}, socket) do
socket =
socket
@ -1348,7 +1302,7 @@ defmodule MvWeb.MemberLive.Index do
|> OverviewQuery.build()
|> Ash.Query.select(@overview_fields)
|> load_custom_field_values(compute_ids_to_load(socket))
|> MembershipFeeStatus.load_cycles_for_members(socket.assigns.show_current_cycle)
|> MembershipFeeStatus.load_cycles_for_members()
|> Ash.Query.load(groups: [:id, :name, :slug])
|> maybe_load_fee_type(socket)
end
@ -1490,8 +1444,6 @@ defmodule MvWeb.MemberLive.Index do
String.starts_with?(field, @custom_field_prefix)
end
defp custom_field_sort?(_), do: false
defp extract_custom_field_ids(visible_custom_fields) do
Enum.map(visible_custom_fields, fn field_string ->
case String.split(field_string, @custom_field_prefix) do
@ -1781,13 +1733,6 @@ defmodule MvWeb.MemberLive.Index do
CustomFieldValueLookup.find_by_field(member, custom_field)
end
def get_boolean_custom_field_value(member, custom_field) do
case get_custom_field_value(member, custom_field) do
nil -> nil
cfv -> extract_boolean_value(cfv.value)
end
end
defp extract_boolean_value(%Ash.Union{value: value, type: :boolean}),
do: extract_boolean_value(value)
@ -1843,14 +1788,6 @@ defmodule MvWeb.MemberLive.Index do
end
end
def format_selected_member_emails(members, selected_members) do
members
|> Enum.filter(fn member ->
MapSet.member?(selected_members, member.id) && member.email && member.email != ""
end)
|> Enum.map(&format_member_email/1)
end
def checkbox_column_click(member), do: JS.push("select_member", value: %{id: member.id})
def format_member_email(member) do
@ -1997,149 +1934,7 @@ defmodule MvWeb.MemberLive.Index do
end
defp assign_export_payload(socket) do
payload = build_export_payload(socket)
payload = ExportPayload.build(socket.assigns)
assign(socket, :export_payload_json, Jason.encode!(payload))
end
defp build_export_payload(socket) do
visible_custom_field_ids = socket.assigns[:visible_custom_field_ids] || []
member_fields_db = socket.assigns[:member_fields_visible_db] || []
member_fields_computed = socket.assigns[:member_fields_visible_computed] || []
# Order DB member fields exactly like the table/constants
ordered_member_fields_db =
Mv.Constants.member_fields()
|> Enum.filter(&(&1 in member_fields_db))
# Order computed fields in canonical order
ordered_computed_fields =
FieldVisibility.computed_member_fields()
|> Enum.filter(&(&1 in member_fields_computed))
member_fields_with_groups =
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 =
socket.assigns.all_custom_fields
|> Enum.map(&to_string(&1.id))
|> Enum.filter(&(&1 in visible_custom_field_ids))
%{
selected_ids: socket.assigns.selected_members |> MapSet.to_list(),
member_fields:
Enum.map(member_fields_with_groups, fn
f when is_atom(f) -> Atom.to_string(f)
f when is_binary(f) -> f
end),
computed_fields: Enum.map(ordered_computed_fields, &Atom.to_string/1),
custom_field_ids: ordered_custom_field_ids,
column_order:
export_column_order(
ordered_member_fields_db,
ordered_computed_fields,
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]),
sort_order: export_sort_order(socket.assigns[:sort_order]),
show_current_cycle: socket.assigns[:show_current_cycle] || false,
cycle_status_filter: export_cycle_status_filter(socket.assigns[:cycle_status_filter]),
boolean_filters: socket.assigns[:boolean_custom_field_filters] || %{}
}
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 fee type is visible but start_date was not in the list, append it
with_extras =
if :membership_fee_type in (member_fields_visible || []) and
:membership_fee_type not in with_extras do
with_extras ++ [:membership_fee_type]
else
with_extras
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"
defp export_cycle_status_filter(_), do: nil
defp export_sort_field(nil), do: nil
defp export_sort_field(f) when is_atom(f), do: Atom.to_string(f)
defp export_sort_field(f) when is_binary(f), do: f
defp export_sort_order(nil), do: nil
defp export_sort_order(:asc), do: "asc"
defp export_sort_order(:desc), do: "desc"
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)
# - 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,
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_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)
)
# If fee type is visible but start_date was not in the list, append it before computed/groups
db_with_extras =
if membership_fee_type_visible and "membership_fee_type" not in db_with_extras do
db_with_extras ++ ["membership_fee_type"]
else
db_with_extras
end
# Any remaining computed fields not inserted above (future-proof)
remaining_computed =
computed_strings
|> Enum.reject(&(&1 in db_with_extras))
result = db_with_extras ++ remaining_computed
result = if groups_visible, do: result ++ ["groups"], else: result
result ++ ordered_custom_field_ids
end
end