feat(member-live): wire date filters into LiveView lifecycle
This commit is contained in:
parent
ddd4a9a878
commit
e3295ab4b5
10 changed files with 1037 additions and 140 deletions
|
|
@ -36,6 +36,8 @@ defmodule MvWeb.MemberLive.Index do
|
|||
alias Mv.MembershipFees
|
||||
alias Mv.MembershipFees.MembershipFeeType
|
||||
alias MvWeb.Helpers.DateFormatter
|
||||
alias MvWeb.MemberLive.Index.CustomFieldValueLookup
|
||||
alias MvWeb.MemberLive.Index.DateFilter
|
||||
alias MvWeb.MemberLive.Index.FieldSelection
|
||||
alias MvWeb.MemberLive.Index.FieldVisibility
|
||||
alias MvWeb.MemberLive.Index.FilterParams
|
||||
|
|
@ -87,6 +89,13 @@ defmodule MvWeb.MemberLive.Index do
|
|||
|> Enum.filter(&(&1.value_type == :boolean))
|
||||
|> Enum.sort_by(& &1.name, :asc)
|
||||
|
||||
# Date-typed custom fields surface in the new "Custom date fields" filter
|
||||
# section and are needed by DateFilter.from_params/2 to validate UUIDs.
|
||||
date_custom_fields =
|
||||
all_custom_fields
|
||||
|> Enum.filter(&(&1.value_type == :date))
|
||||
|> Enum.sort_by(& &1.name, :asc)
|
||||
|
||||
# Load groups for filter dropdown (sorted by name)
|
||||
groups =
|
||||
Mv.Membership.Group
|
||||
|
|
@ -143,6 +152,8 @@ defmodule MvWeb.MemberLive.Index do
|
|||
|> assign(:custom_fields_visible, custom_fields_visible)
|
||||
|> assign(:all_custom_fields, all_custom_fields)
|
||||
|> assign(:boolean_custom_fields, boolean_custom_fields)
|
||||
|> assign(:date_custom_fields, date_custom_fields)
|
||||
|> assign(:date_filters, DateFilter.default())
|
||||
|> assign(:all_available_fields, all_available_fields)
|
||||
|> assign(:user_field_selection, initial_selection)
|
||||
|> assign(:fields_in_url?, false)
|
||||
|
|
@ -448,6 +459,25 @@ defmodule MvWeb.MemberLive.Index do
|
|||
{:noreply, push_patch(socket, to: new_path, replace: true)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:date_filters_changed, new_date_filters}, socket) do
|
||||
socket =
|
||||
socket
|
||||
|> assign(:date_filters, new_date_filters)
|
||||
|> load_members()
|
||||
|> update_selection_assigns()
|
||||
|
||||
query_params =
|
||||
build_query_params(opts_for_query_params(socket, %{date_filters: new_date_filters}))
|
||||
|> maybe_add_field_selection(
|
||||
socket.assigns[:user_field_selection],
|
||||
socket.assigns[:fields_in_url?] || false
|
||||
)
|
||||
|
||||
new_path = ~p"/members?#{query_params}"
|
||||
{:noreply, push_patch(socket, to: new_path, replace: true)}
|
||||
end
|
||||
|
||||
# Backward compatibility: tuple form delegates to map form
|
||||
def handle_info({:reset_all_filters, cycle_status_filter, boolean_filters}, socket) do
|
||||
handle_info(
|
||||
|
|
@ -502,6 +532,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
|> assign(:group_filters, Map.get(opts, :group_filters, %{}))
|
||||
|> assign(:fee_type_filters, Map.get(opts, :fee_type_filters, %{}))
|
||||
|> assign(:boolean_custom_field_filters, Map.get(opts, :boolean_filters, %{}))
|
||||
|> assign(:date_filters, Map.get(opts, :date_filters, DateFilter.default()))
|
||||
|> load_members()
|
||||
|> update_selection_assigns()
|
||||
|
||||
|
|
@ -632,6 +663,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
|> maybe_update_group_filters(params)
|
||||
|> maybe_update_fee_type_filters(params)
|
||||
|> maybe_update_boolean_filters(params)
|
||||
|> maybe_update_date_filters(params)
|
||||
|> maybe_update_show_current_cycle(params)
|
||||
|> assign(:fields_in_url?, fields_in_url?)
|
||||
|> assign(:query, params["query"])
|
||||
|
|
@ -683,7 +715,8 @@ defmodule MvWeb.MemberLive.Index do
|
|||
socket.assigns.show_current_cycle,
|
||||
socket.assigns.boolean_custom_field_filters,
|
||||
socket.assigns.user_field_selection,
|
||||
socket.assigns[:visible_custom_field_ids] || []
|
||||
socket.assigns[:visible_custom_field_ids] || [],
|
||||
socket.assigns[:date_filters]
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -783,7 +816,12 @@ defmodule MvWeb.MemberLive.Index do
|
|||
base_params = add_group_filters(base_params, opts.group_filters || %{})
|
||||
base_params = add_fee_type_filters(base_params, opts.fee_type_filters || %{})
|
||||
base_params = add_show_current_cycle(base_params, opts.show_current_cycle)
|
||||
add_boolean_filters(base_params, opts.boolean_filters || %{})
|
||||
base_params = add_boolean_filters(base_params, opts.boolean_filters || %{})
|
||||
add_date_filters(base_params, opts.date_filters)
|
||||
end
|
||||
|
||||
defp add_date_filters(params, date_filters) do
|
||||
Map.merge(params, DateFilter.to_params(date_filters))
|
||||
end
|
||||
|
||||
defp opts_for_query_params(socket, overrides \\ %{}) do
|
||||
|
|
@ -795,7 +833,8 @@ defmodule MvWeb.MemberLive.Index do
|
|||
group_filters: socket.assigns[:group_filters] || %{},
|
||||
show_current_cycle: socket.assigns.show_current_cycle,
|
||||
boolean_filters: socket.assigns.boolean_custom_field_filters || %{},
|
||||
fee_type_filters: socket.assigns[:fee_type_filters] || %{}
|
||||
fee_type_filters: socket.assigns[:fee_type_filters] || %{},
|
||||
date_filters: socket.assigns.date_filters
|
||||
}
|
||||
|> Map.merge(overrides)
|
||||
end
|
||||
|
|
@ -941,26 +980,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
|> Ash.Query.new()
|
||||
|> Ash.Query.select(@overview_fields)
|
||||
|
||||
visible_custom_field_ids = socket.assigns[:visible_custom_field_ids] || []
|
||||
|
||||
boolean_custom_fields_map =
|
||||
socket.assigns.boolean_custom_fields
|
||||
|> Map.new(fn cf -> {to_string(cf.id), cf} end)
|
||||
|
||||
active_boolean_filter_ids =
|
||||
socket.assigns.boolean_custom_field_filters
|
||||
|> Map.keys()
|
||||
|> Enum.filter(fn id_str ->
|
||||
String.length(id_str) <= @max_uuid_length &&
|
||||
match?({:ok, _}, Ecto.UUID.cast(id_str)) &&
|
||||
Map.has_key?(boolean_custom_fields_map, id_str)
|
||||
end)
|
||||
|
||||
ids_to_load =
|
||||
(visible_custom_field_ids ++ active_boolean_filter_ids)
|
||||
|> Enum.uniq()
|
||||
|
||||
query = load_custom_field_values(query, ids_to_load)
|
||||
query = load_custom_field_values(query, compute_ids_to_load(socket))
|
||||
|
||||
query = MembershipFeeStatus.load_cycles_for_members(query, socket.assigns.show_current_cycle)
|
||||
|
||||
|
|
@ -984,6 +1004,13 @@ defmodule MvWeb.MemberLive.Index do
|
|||
query =
|
||||
apply_fee_type_filters(query, socket.assigns[:fee_type_filters], socket.assigns[:fee_types])
|
||||
|
||||
# Built-in date filters (join_date, exit_date) are pushed to the DB so
|
||||
# excluded rows never reach the BEAM. The active_only default is part of
|
||||
# this — fresh load returns only members without an exit_date or with an
|
||||
# exit_date strictly in the future.
|
||||
query =
|
||||
DateFilter.apply_ash_filter(query, socket.assigns.date_filters)
|
||||
|
||||
# Use ALL custom fields for sorting (not just show_in_overview subset)
|
||||
custom_fields_for_sort = socket.assigns.all_custom_fields
|
||||
|
||||
|
|
@ -1003,21 +1030,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
# Custom field values are already filtered at the database level in load_custom_field_values/2
|
||||
# No need for in-memory filtering anymore
|
||||
|
||||
# Apply cycle status filter if set
|
||||
members =
|
||||
apply_cycle_status_filter(
|
||||
members,
|
||||
socket.assigns.cycle_status_filter,
|
||||
socket.assigns.show_current_cycle
|
||||
)
|
||||
|
||||
# Apply boolean custom field filters if set
|
||||
members =
|
||||
apply_boolean_custom_field_filters(
|
||||
members,
|
||||
socket.assigns.boolean_custom_field_filters,
|
||||
socket.assigns.all_custom_fields
|
||||
)
|
||||
members = apply_in_memory_filters(members, socket)
|
||||
|
||||
# Sort in memory if needed (custom fields, groups, group_count; computed fields are blocked)
|
||||
# Note: :groups is in computed_member_fields() but can be sorted in-memory, so we only block :membership_fee_status
|
||||
|
|
@ -1037,6 +1050,55 @@ defmodule MvWeb.MemberLive.Index do
|
|||
assign(socket, :members, members)
|
||||
end
|
||||
|
||||
# Collects every custom field UUID whose values must be loaded for a given
|
||||
# render — visible columns plus any active boolean or date filter. Kept as a
|
||||
# standalone helper so load_members/1 stays under the credo complexity bar.
|
||||
defp compute_ids_to_load(socket) do
|
||||
visible_custom_field_ids = socket.assigns[:visible_custom_field_ids] || []
|
||||
|
||||
boolean_custom_fields_map =
|
||||
socket.assigns.boolean_custom_fields
|
||||
|> Map.new(fn cf -> {to_string(cf.id), cf} end)
|
||||
|
||||
active_boolean_filter_ids =
|
||||
socket.assigns.boolean_custom_field_filters
|
||||
|> Map.keys()
|
||||
|> Enum.filter(fn id_str ->
|
||||
String.length(id_str) <= @max_uuid_length &&
|
||||
match?({:ok, _}, Ecto.UUID.cast(id_str)) &&
|
||||
Map.has_key?(boolean_custom_fields_map, id_str)
|
||||
end)
|
||||
|
||||
date_custom_fields = socket.assigns[:date_custom_fields] || []
|
||||
|
||||
active_date_filter_ids =
|
||||
DateFilter.active_custom_field_ids(
|
||||
socket.assigns.date_filters,
|
||||
date_custom_fields
|
||||
)
|
||||
|
||||
(visible_custom_field_ids ++ active_boolean_filter_ids ++ active_date_filter_ids)
|
||||
|> Enum.uniq()
|
||||
end
|
||||
|
||||
# Post-DB filtering: cycle status, boolean custom fields, and custom date
|
||||
# fields. Date custom fields are last so they see the already-narrowed list.
|
||||
defp apply_in_memory_filters(members, socket) do
|
||||
members
|
||||
|> apply_cycle_status_filter(
|
||||
socket.assigns.cycle_status_filter,
|
||||
socket.assigns.show_current_cycle
|
||||
)
|
||||
|> apply_boolean_custom_field_filters(
|
||||
socket.assigns.boolean_custom_field_filters,
|
||||
socket.assigns.all_custom_fields
|
||||
)
|
||||
|> DateFilter.apply_in_memory(
|
||||
socket.assigns.date_filters,
|
||||
socket.assigns[:date_custom_fields] || []
|
||||
)
|
||||
end
|
||||
|
||||
defp load_custom_field_values(query, []), do: query
|
||||
|
||||
defp load_custom_field_values(query, custom_field_ids) do
|
||||
|
|
@ -1649,24 +1711,22 @@ defmodule MvWeb.MemberLive.Index do
|
|||
|
||||
defp maybe_update_show_current_cycle(socket, _params), do: socket
|
||||
|
||||
# URL params are the source of truth for filter state on every navigation.
|
||||
# When no date filter params are present, this falls through to the
|
||||
# active_only default — exactly the spec behavior for fresh load (§1.1).
|
||||
defp maybe_update_date_filters(socket, params) when is_map(params) do
|
||||
date_custom_fields = socket.assigns[:date_custom_fields] || []
|
||||
assign(socket, :date_filters, DateFilter.from_params(params, date_custom_fields))
|
||||
end
|
||||
|
||||
defp maybe_update_date_filters(socket, _params), do: socket
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Custom Field Value Helpers
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def get_custom_field_value(member, custom_field) do
|
||||
case member.custom_field_values do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
values when is_list(values) ->
|
||||
Enum.find(values, fn cfv ->
|
||||
cfv.custom_field_id == custom_field.id or
|
||||
(match?(%{custom_field: %{id: _}}, cfv) && cfv.custom_field.id == custom_field.id)
|
||||
end)
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
CustomFieldValueLookup.find_by_field(member, custom_field)
|
||||
end
|
||||
|
||||
def get_boolean_custom_field_value(member, custom_field) do
|
||||
|
|
@ -1725,29 +1785,12 @@ defmodule MvWeb.MemberLive.Index do
|
|||
end
|
||||
|
||||
defp matches_filter?(member, custom_field_id_str, filter_value) do
|
||||
case find_custom_field_value_by_id(member, custom_field_id_str) do
|
||||
case CustomFieldValueLookup.find_by_id(member, custom_field_id_str) do
|
||||
nil -> false
|
||||
cfv -> extract_boolean_value(cfv.value) == filter_value
|
||||
end
|
||||
end
|
||||
|
||||
defp find_custom_field_value_by_id(member, custom_field_id_str) do
|
||||
case member.custom_field_values do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
values when is_list(values) ->
|
||||
Enum.find(values, fn cfv ->
|
||||
to_string(cfv.custom_field_id) == custom_field_id_str or
|
||||
(match?(%{custom_field: %{id: _}}, cfv) &&
|
||||
to_string(cfv.custom_field.id) == custom_field_id_str)
|
||||
end)
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def format_selected_member_emails(members, selected_members) do
|
||||
members
|
||||
|> Enum.filter(fn member ->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue