773 lines
24 KiB
Elixir
773 lines
24 KiB
Elixir
defmodule MvWeb.MemberLive.Index do
|
|
@moduledoc """
|
|
LiveView for displaying and managing the member list.
|
|
|
|
## Features
|
|
- Full-text search across member profiles using PostgreSQL tsvector
|
|
- Sortable columns (name, email, address fields)
|
|
- Bulk selection for future batch operations
|
|
- Real-time updates via LiveView
|
|
- Bookmarkable URLs with query parameters
|
|
|
|
## URL Parameters
|
|
- `query` - Search query string for full-text search
|
|
- `sort_field` - Field to sort by (e.g., :first_name, :email, :join_date)
|
|
- `sort_order` - Sort direction (:asc or :desc)
|
|
|
|
## Events
|
|
- `delete` - Remove a member from the database
|
|
- `select_member` - Toggle individual member selection
|
|
- `select_all` - Toggle selection of all visible members
|
|
|
|
## Implementation Notes
|
|
- Search uses PostgreSQL full-text search (plainto_tsquery)
|
|
- Sort state is synced with URL for bookmarkability
|
|
- Components communicate via `handle_info` for decoupling
|
|
"""
|
|
use MvWeb, :live_view
|
|
|
|
require Ash.Query
|
|
import Ash.Expr
|
|
|
|
alias MvWeb.MemberLive.Index.Formatter
|
|
|
|
# Prefix used in sort field names for custom fields (e.g., "custom_field_<id>")
|
|
@custom_field_prefix "custom_field_"
|
|
|
|
@doc """
|
|
Initializes the LiveView state.
|
|
|
|
Sets up initial assigns for page title, search query, sort configuration,
|
|
and member selection. Actual data loading happens in `handle_params/3`.
|
|
"""
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
# Load custom fields that should be shown in overview
|
|
# Note: Using Ash.read! (bang version) - errors will be handled by Phoenix LiveView
|
|
# and result in a 500 error page. This is appropriate for LiveViews where errors
|
|
# should be visible to the user rather than silently failing.
|
|
custom_fields_visible =
|
|
Mv.Membership.CustomField
|
|
|> Ash.Query.filter(expr(show_in_overview == true))
|
|
|> Ash.Query.sort(name: :asc)
|
|
|> Ash.read!()
|
|
|
|
socket =
|
|
socket
|
|
|> assign(:page_title, gettext("Members"))
|
|
|> assign(:query, "")
|
|
|> assign_new(:sort_field, fn -> :first_name end)
|
|
|> assign_new(:sort_order, fn -> :asc end)
|
|
|> assign(:selected_members, [])
|
|
|> assign(:custom_fields_visible, custom_fields_visible)
|
|
|> assign(:member_field_configurations, get_member_field_configurations())
|
|
|> assign(:member_fields_visible, get_visible_member_fields())
|
|
|
|
# We call handle params to use the query from the URL
|
|
{:ok, socket}
|
|
end
|
|
|
|
# -----------------------------------------------------------------
|
|
# Handle Events
|
|
# -----------------------------------------------------------------
|
|
|
|
@doc """
|
|
Handles member-related UI events.
|
|
|
|
## Supported events:
|
|
- `"delete"` - Removes a member from the database
|
|
- `"select_member"` - Toggles individual member selection
|
|
- `"select_all"` - Toggles selection of all visible members
|
|
"""
|
|
@impl true
|
|
def handle_event("delete", %{"id" => id}, socket) do
|
|
# Note: Using bang versions (!) - errors will be handled by Phoenix LiveView
|
|
# This ensures users see error messages if deletion fails (e.g., permission denied)
|
|
member = Ash.get!(Mv.Membership.Member, id)
|
|
Ash.destroy!(member)
|
|
|
|
updated_members = Enum.reject(socket.assigns.members, &(&1.id == id))
|
|
{:noreply, assign(socket, :members, updated_members)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("select_member", %{"id" => id}, socket) do
|
|
selected =
|
|
if id in socket.assigns.selected_members do
|
|
List.delete(socket.assigns.selected_members, id)
|
|
else
|
|
[id | socket.assigns.selected_members]
|
|
end
|
|
|
|
{:noreply, assign(socket, :selected_members, selected)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("select_all", _params, socket) do
|
|
members = socket.assigns.members
|
|
|
|
all_ids = Enum.map(members, & &1.id)
|
|
|
|
selected =
|
|
if Enum.sort(socket.assigns.selected_members) == Enum.sort(all_ids) do
|
|
[]
|
|
else
|
|
all_ids
|
|
end
|
|
|
|
{:noreply, assign(socket, :selected_members, selected)}
|
|
end
|
|
|
|
# -----------------------------------------------------------------
|
|
# Handle Infos from Child Components
|
|
# -----------------------------------------------------------------
|
|
|
|
@doc """
|
|
Handles messages from child components.
|
|
|
|
## Supported messages:
|
|
- `{:sort, field}` - Sort event from SortHeaderComponent. Updates sort field/order and syncs URL
|
|
- `{:search_changed, query}` - Search event from SearchBarComponent. Filters members and syncs URL
|
|
"""
|
|
@impl true
|
|
def handle_info({:sort, field_str}, socket) do
|
|
# Handle both atom and string field names (for custom fields)
|
|
field =
|
|
try do
|
|
String.to_existing_atom(field_str)
|
|
rescue
|
|
ArgumentError -> field_str
|
|
end
|
|
|
|
{new_field, new_order} = determine_new_sort(field, socket)
|
|
|
|
socket
|
|
|> update_sort_components(socket.assigns.sort_field, new_field, new_order)
|
|
|> push_sort_url(new_field, new_order)
|
|
end
|
|
|
|
@impl true
|
|
def handle_info({:search_changed, q}, socket) do
|
|
socket = load_members(socket, q)
|
|
|
|
existing_field_query = socket.assigns.sort_field
|
|
existing_sort_query = socket.assigns.sort_order
|
|
|
|
# Build the URL with queries
|
|
query_params = %{
|
|
"query" => q,
|
|
"sort_field" => existing_field_query,
|
|
"sort_order" => existing_sort_query
|
|
}
|
|
|
|
# Set the new path with params
|
|
new_path = ~p"/members?#{query_params}"
|
|
|
|
# Push the new URL
|
|
{:noreply,
|
|
push_patch(socket,
|
|
to: new_path,
|
|
replace: true
|
|
)}
|
|
end
|
|
|
|
# -----------------------------------------------------------------
|
|
# Handle Params from the URL
|
|
# -----------------------------------------------------------------
|
|
@doc """
|
|
Handles URL parameter changes.
|
|
|
|
Parses query parameters for search query, sort field, and sort order,
|
|
then loads members accordingly. This enables bookmarkable URLs and
|
|
browser back/forward navigation.
|
|
"""
|
|
@impl true
|
|
def handle_params(params, _url, socket) do
|
|
socket =
|
|
socket
|
|
|> maybe_update_search(params)
|
|
|> maybe_update_sort(params)
|
|
|> load_members(params["query"])
|
|
|> prepare_dynamic_cols()
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
# Prepares dynamic column definitions for custom fields that should be shown in the overview.
|
|
#
|
|
# Creates a list of column definitions, each containing:
|
|
# - `:custom_field` - The CustomField resource
|
|
# - `:render` - A function that formats the custom field value for a given member
|
|
#
|
|
# Returns the socket with `:dynamic_cols` assigned.
|
|
defp prepare_dynamic_cols(socket) do
|
|
dynamic_cols =
|
|
Enum.map(socket.assigns.custom_fields_visible, fn custom_field ->
|
|
%{
|
|
custom_field: custom_field,
|
|
render: fn member ->
|
|
case get_custom_field_value(member, custom_field) do
|
|
nil ->
|
|
""
|
|
|
|
cfv ->
|
|
Formatter.format_custom_field_value(cfv.value, custom_field)
|
|
end
|
|
end
|
|
}
|
|
end)
|
|
|
|
assign(socket, :dynamic_cols, dynamic_cols)
|
|
end
|
|
|
|
# -------------------------------------------------------------
|
|
# FUNCTIONS
|
|
# -------------------------------------------------------------
|
|
|
|
# Determines new sort field and order based on current state
|
|
defp determine_new_sort(field, socket) do
|
|
if socket.assigns.sort_field == field do
|
|
{field, toggle_order(socket.assigns.sort_order)}
|
|
else
|
|
{field, :asc}
|
|
end
|
|
end
|
|
|
|
# Updates both the active and old SortHeader components
|
|
defp update_sort_components(socket, old_field, new_field, new_order) do
|
|
active_id = to_sort_id(new_field)
|
|
old_id = to_sort_id(old_field)
|
|
|
|
# Update the new SortHeader
|
|
send_update(MvWeb.Components.SortHeaderComponent,
|
|
id: active_id,
|
|
sort_field: new_field,
|
|
sort_order: new_order
|
|
)
|
|
|
|
# Reset the current SortHeader
|
|
send_update(MvWeb.Components.SortHeaderComponent,
|
|
id: old_id,
|
|
sort_field: new_field,
|
|
sort_order: new_order
|
|
)
|
|
|
|
socket
|
|
end
|
|
|
|
# Converts a field (atom or string) to a sort component ID atom
|
|
# Handles both existing atoms and strings that need to be converted
|
|
defp to_sort_id(field) when is_binary(field) do
|
|
try do
|
|
String.to_existing_atom("sort_#{field}")
|
|
rescue
|
|
ArgumentError -> :"sort_#{field}"
|
|
end
|
|
end
|
|
|
|
defp to_sort_id(field) when is_atom(field) do
|
|
:"sort_#{field}"
|
|
end
|
|
|
|
# Builds sort URL and pushes navigation patch
|
|
defp push_sort_url(socket, field, order) do
|
|
field_str =
|
|
if is_atom(field) do
|
|
Atom.to_string(field)
|
|
else
|
|
field
|
|
end
|
|
|
|
query_params = %{
|
|
"query" => socket.assigns.query,
|
|
"sort_field" => field_str,
|
|
"sort_order" => Atom.to_string(order)
|
|
}
|
|
|
|
new_path = ~p"/members?#{query_params}"
|
|
|
|
{:noreply,
|
|
push_patch(socket,
|
|
to: new_path,
|
|
replace: true
|
|
)}
|
|
end
|
|
|
|
# Loads members from the database with custom field values and applies search/sort filters.
|
|
#
|
|
# Process:
|
|
# 1. Builds base query with selected fields
|
|
# 2. Loads custom field values for visible custom fields (filtered at database level)
|
|
# 3. Applies search filter if provided
|
|
# 4. Applies sorting (database-level for regular fields, in-memory for custom fields)
|
|
#
|
|
# Performance Considerations:
|
|
# - Database-level filtering: Custom field values are filtered directly in the database
|
|
# using Ash relationship filters, reducing memory usage and improving performance.
|
|
# - In-memory sorting: Custom field sorting is done in memory after loading.
|
|
# This is suitable for small to medium datasets (<1000 members).
|
|
# For larger datasets, consider implementing database-level sorting or pagination.
|
|
# - No pagination: All matching members are loaded at once. For large result sets,
|
|
# consider implementing pagination (see Issue #165).
|
|
#
|
|
# Returns the socket with `:members` assigned.
|
|
defp load_members(socket, search_query) do
|
|
query =
|
|
Mv.Membership.Member
|
|
|> Ash.Query.new()
|
|
|> Ash.Query.select([
|
|
:id,
|
|
:first_name,
|
|
:last_name,
|
|
:email,
|
|
:street,
|
|
:house_number,
|
|
:postal_code,
|
|
:city,
|
|
:phone_number,
|
|
:join_date
|
|
])
|
|
|
|
# Load custom field values for visible custom fields
|
|
custom_field_ids_list = Enum.map(socket.assigns.custom_fields_visible, & &1.id)
|
|
query = load_custom_field_values(query, custom_field_ids_list)
|
|
|
|
# Apply the search filter first
|
|
query = apply_search_filter(query, search_query)
|
|
|
|
# Apply sorting based on current socket state
|
|
# For custom fields, we sort after loading
|
|
{query, sort_after_load} =
|
|
maybe_sort(
|
|
query,
|
|
socket.assigns.sort_field,
|
|
socket.assigns.sort_order,
|
|
socket.assigns.custom_fields_visible
|
|
)
|
|
|
|
# Note: Using Ash.read! - errors will be handled by Phoenix LiveView
|
|
# This is appropriate for data loading in LiveViews
|
|
members = Ash.read!(query)
|
|
|
|
# Custom field values are already filtered at the database level in load_custom_field_values/2
|
|
# No need for in-memory filtering anymore
|
|
|
|
# Sort in memory if needed (for custom fields)
|
|
members =
|
|
if sort_after_load do
|
|
sort_members_in_memory(
|
|
members,
|
|
socket.assigns.sort_field,
|
|
socket.assigns.sort_order,
|
|
socket.assigns.custom_fields_visible
|
|
)
|
|
else
|
|
members
|
|
end
|
|
|
|
assign(socket, :members, members)
|
|
end
|
|
|
|
# Load custom field values for the given custom field IDs
|
|
#
|
|
# Filters custom field values directly in the database using Ash relationship filters.
|
|
# This is more efficient than loading all values and filtering in memory.
|
|
#
|
|
# Performance: Database-level filtering reduces:
|
|
# - Memory usage (only visible custom field values are loaded)
|
|
# - Network transfer (less data from database to application)
|
|
# - Processing time (no need to iterate through all members and filter)
|
|
defp load_custom_field_values(query, []) do
|
|
query
|
|
end
|
|
|
|
defp load_custom_field_values(query, custom_field_ids) when length(custom_field_ids) > 0 do
|
|
# Filter custom field values at the database level using Ash relationship query
|
|
# This ensures only visible custom field values are loaded
|
|
custom_field_values_query =
|
|
Mv.Membership.CustomFieldValue
|
|
|> Ash.Query.filter(expr(custom_field_id in ^custom_field_ids))
|
|
|> Ash.Query.load(custom_field: [:id, :name, :value_type])
|
|
|
|
query
|
|
|> Ash.Query.load(custom_field_values: custom_field_values_query)
|
|
end
|
|
|
|
# -------------------------------------------------------------
|
|
# Helper Functions
|
|
# -------------------------------------------------------------
|
|
|
|
# Function to apply search query
|
|
defp apply_search_filter(query, search_query) do
|
|
if search_query && String.trim(search_query) != "" do
|
|
query
|
|
|> Mv.Membership.Member.fuzzy_search(%{
|
|
query: search_query
|
|
})
|
|
else
|
|
query
|
|
end
|
|
end
|
|
|
|
# Functions to toggle sorting order
|
|
defp toggle_order(:asc), do: :desc
|
|
defp toggle_order(:desc), do: :asc
|
|
defp toggle_order(nil), do: :asc
|
|
|
|
# Function to sort the column if needed
|
|
# Returns {query, sort_after_load} where sort_after_load is true if we need to sort in memory
|
|
defp maybe_sort(query, nil, _, _), do: {query, false}
|
|
|
|
defp maybe_sort(query, field, order, _custom_fields) when not is_nil(field) do
|
|
if custom_field_sort?(field) do
|
|
# Custom fields need to be sorted in memory after loading
|
|
{query, true}
|
|
else
|
|
# Only sort by atom fields (regular member fields) in database
|
|
if is_atom(field) do
|
|
{Ash.Query.sort(query, [{field, order}]), false}
|
|
else
|
|
{query, false}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp maybe_sort(query, _, _, _), do: {query, false}
|
|
|
|
# Validate that a field is sortable
|
|
defp valid_sort_field?(field) when is_atom(field) do
|
|
valid_fields = [
|
|
:first_name,
|
|
:last_name,
|
|
:email,
|
|
:street,
|
|
:house_number,
|
|
:postal_code,
|
|
:city,
|
|
:phone_number,
|
|
:join_date
|
|
]
|
|
|
|
field in valid_fields or custom_field_sort?(field)
|
|
end
|
|
|
|
defp valid_sort_field?(field) when is_binary(field) do
|
|
custom_field_sort?(field)
|
|
end
|
|
|
|
defp valid_sort_field?(_), do: false
|
|
|
|
# Check if field is a custom field sort field (format: custom_field_<id>)
|
|
defp custom_field_sort?(field) when is_atom(field) do
|
|
field_str = Atom.to_string(field)
|
|
String.starts_with?(field_str, @custom_field_prefix)
|
|
end
|
|
|
|
defp custom_field_sort?(field) when is_binary(field) do
|
|
String.starts_with?(field, @custom_field_prefix)
|
|
end
|
|
|
|
defp custom_field_sort?(_), do: false
|
|
|
|
# Extracts the custom field ID from a sort field name.
|
|
#
|
|
# Sort fields for custom fields use the format: "custom_field_<id>"
|
|
# This function extracts the ID part.
|
|
#
|
|
# Examples:
|
|
# extract_custom_field_id("custom_field_123") -> "123"
|
|
# extract_custom_field_id(:custom_field_123) -> "123"
|
|
# extract_custom_field_id("first_name") -> nil
|
|
defp extract_custom_field_id(field) when is_atom(field) do
|
|
field_str = Atom.to_string(field)
|
|
extract_custom_field_id(field_str)
|
|
end
|
|
|
|
defp extract_custom_field_id(field) when is_binary(field) do
|
|
case String.split(field, @custom_field_prefix) do
|
|
["", id_str] -> id_str
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
defp extract_custom_field_id(_), do: nil
|
|
|
|
# Sorts members in memory by a custom field value.
|
|
#
|
|
# Process:
|
|
# 1. Extracts custom field ID from sort field name
|
|
# 2. Finds the corresponding CustomField resource
|
|
# 3. Splits members into those with values and those without
|
|
# 4. Sorts members with values by the extracted value
|
|
# 5. Combines: sorted values first, then NULL/empty values at the end
|
|
#
|
|
# Performance Note:
|
|
# This function sorts in memory, which is suitable for small to medium datasets (<1000 members).
|
|
# For larger datasets, consider implementing database-level sorting or pagination.
|
|
#
|
|
# Parameters:
|
|
# - `members` - List of Member resources to sort
|
|
# - `field` - Sort field name (format: "custom_field_<id>" or atom)
|
|
# - `order` - Sort order (`:asc` or `:desc`)
|
|
# - `custom_fields` - List of visible CustomField resources
|
|
#
|
|
# Returns the sorted list of members.
|
|
defp sort_members_in_memory(members, field, order, custom_fields) do
|
|
custom_field_id_str = extract_custom_field_id(field)
|
|
|
|
case custom_field_id_str do
|
|
nil ->
|
|
members
|
|
|
|
id_str ->
|
|
sort_members_by_custom_field(members, id_str, order, custom_fields)
|
|
end
|
|
end
|
|
|
|
# Sorts members by a specific custom field ID
|
|
defp sort_members_by_custom_field(members, id_str, order, custom_fields) do
|
|
custom_field = find_custom_field_by_id(custom_fields, id_str)
|
|
|
|
case custom_field do
|
|
nil ->
|
|
members
|
|
|
|
cf ->
|
|
sort_members_with_custom_field(members, cf, order)
|
|
end
|
|
end
|
|
|
|
# Finds a custom field by matching its ID string
|
|
defp find_custom_field_by_id(custom_fields, id_str) do
|
|
Enum.find(custom_fields, fn cf ->
|
|
to_string(cf.id) == id_str
|
|
end)
|
|
end
|
|
|
|
# Sorts members that have a specific custom field
|
|
defp sort_members_with_custom_field(members, custom_field, order) do
|
|
# Split members into those with values and those without (NULL/empty)
|
|
{members_with_values, members_without_values} =
|
|
split_members_by_value_presence(members, custom_field)
|
|
|
|
# Sort members with values
|
|
sorted_with_values = sort_members_with_values(members_with_values, custom_field, order)
|
|
|
|
# Combine: sorted values first, then NULL/empty values at the end
|
|
sorted_with_values ++ members_without_values
|
|
end
|
|
|
|
# Splits members into those with values and those without
|
|
defp split_members_by_value_presence(members, custom_field) do
|
|
Enum.split_with(members, fn member ->
|
|
has_non_empty_value?(member, custom_field)
|
|
end)
|
|
end
|
|
|
|
# Checks if a member has a non-empty value for the custom field
|
|
defp has_non_empty_value?(member, custom_field) do
|
|
case get_custom_field_value(member, custom_field) do
|
|
nil ->
|
|
false
|
|
|
|
cfv ->
|
|
extracted = extract_sort_value(cfv.value, custom_field.value_type)
|
|
not empty_value?(extracted, custom_field.value_type)
|
|
end
|
|
end
|
|
|
|
# Sorts members that have values for the custom field
|
|
defp sort_members_with_values(members_with_values, custom_field, order) 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)
|
|
end)
|
|
|
|
# For DESC, reverse only the members with values
|
|
if order == :desc do
|
|
Enum.reverse(sorted)
|
|
else
|
|
sorted
|
|
end
|
|
end
|
|
|
|
# Extracts a sortable value from a custom field value based on its type.
|
|
#
|
|
# Handles different value formats:
|
|
# - `%Ash.Union{}` - Extracts value and type from union
|
|
# - Direct values - Returns as-is for primitive types
|
|
#
|
|
# Returns the extracted value suitable for sorting.
|
|
defp extract_sort_value(%Ash.Union{value: value, type: type}, _expected_type) do
|
|
extract_sort_value(value, type)
|
|
end
|
|
|
|
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)
|
|
|
|
# Check if a value is considered empty (NULL or empty string)
|
|
defp empty_value?(value, :string) when is_binary(value) do
|
|
String.trim(value) == ""
|
|
end
|
|
|
|
defp empty_value?(value, :email) when is_binary(value) do
|
|
String.trim(value) == ""
|
|
end
|
|
|
|
defp empty_value?(_value, _type), do: false
|
|
|
|
# Normalize sort value for DESC order
|
|
# For DESC, we sort ascending first, then reverse the list
|
|
# This function is kept for consistency but doesn't need to invert values
|
|
defp normalize_sort_value(value, _order), do: value
|
|
|
|
# Updates sort field and order from URL parameters if present.
|
|
#
|
|
# Validates the sort field and order, falling back to defaults if invalid.
|
|
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)
|
|
|
|
socket
|
|
|> assign(:sort_field, field)
|
|
|> assign(:sort_order, order)
|
|
end
|
|
|
|
defp maybe_update_sort(socket, _), do: socket
|
|
|
|
# Determine sort field from URL parameter, validating against allowed fields
|
|
defp determine_field(default, ""), do: default
|
|
defp determine_field(default, nil), do: default
|
|
|
|
# Determines the valid sort field from a URL parameter.
|
|
#
|
|
# Validates the field against allowed sort fields (regular member fields or custom fields).
|
|
# Falls back to default if the field is invalid.
|
|
#
|
|
# Parameters:
|
|
# - `default` - Default field to use if validation fails
|
|
# - `sf` - Sort field from URL (can be atom, string, nil, or empty string)
|
|
#
|
|
# Returns a valid sort field (atom or string for custom fields).
|
|
defp determine_field(default, sf) when is_binary(sf) do
|
|
# Check if it's a custom field sort (starts with "custom_field_")
|
|
if custom_field_sort?(sf) do
|
|
if valid_sort_field?(sf), do: sf, else: default
|
|
else
|
|
# Try to convert to atom for regular fields
|
|
try do
|
|
atom = String.to_existing_atom(sf)
|
|
if valid_sort_field?(atom), do: atom, else: default
|
|
rescue
|
|
ArgumentError -> default
|
|
end
|
|
end
|
|
end
|
|
|
|
defp determine_field(default, sf) when is_atom(sf) do
|
|
if valid_sort_field?(sf), do: sf, else: default
|
|
end
|
|
|
|
defp determine_field(default, _), do: default
|
|
|
|
# Determines the valid sort order from a URL parameter.
|
|
#
|
|
# Validates that the order is either "asc" or "desc", falling back to default if invalid.
|
|
#
|
|
# Parameters:
|
|
# - `default` - Default order to use if validation fails
|
|
# - `so` - Sort order from URL (string, atom, nil, or empty string)
|
|
#
|
|
# Returns `:asc` or `:desc`.
|
|
defp determine_order(default, so) do
|
|
case so do
|
|
"" -> default
|
|
nil -> default
|
|
so when so in ["asc", "desc"] -> String.to_atom(so)
|
|
_ -> default
|
|
end
|
|
end
|
|
|
|
# Function to update search parameters
|
|
defp maybe_update_search(socket, %{"query" => query}) when query != "" do
|
|
assign(socket, :query, query)
|
|
end
|
|
|
|
defp maybe_update_search(socket, _params) do
|
|
# Keep the previous search query if no new one is provided
|
|
socket
|
|
end
|
|
|
|
# -------------------------------------------------------------
|
|
# Helper Functions for Custom Field Values
|
|
# -------------------------------------------------------------
|
|
|
|
# Retrieves the custom field value for a specific member and custom field.
|
|
#
|
|
# Searches through the member's `custom_field_values` relationship to find
|
|
# the value matching the given custom field.
|
|
#
|
|
# Returns:
|
|
# - `%CustomFieldValue{}` if found
|
|
# - `nil` if not found or if member has no custom field values
|
|
#
|
|
# Examples:
|
|
# get_custom_field_value(member, custom_field) -> %CustomFieldValue{...}
|
|
# get_custom_field_value(member, non_existent_field) -> nil
|
|
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
|
|
(cfv.custom_field && cfv.custom_field.id == custom_field.id)
|
|
end)
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Gets the configuration for all member fields with their show_in_overview values.
|
|
#
|
|
# Reads the visibility configuration from Settings and returns a map with all member fields
|
|
# and their show_in_overview values (true or false). Fields not configured in settings
|
|
# default to true.
|
|
#
|
|
# Returns a map: %{field_name => show_in_overview}
|
|
#
|
|
# This can be used for:
|
|
# - Rendering the overview (filtering visible fields)
|
|
# - UI configuration dropdowns (showing all fields with their current state)
|
|
# - Dynamic field management
|
|
#
|
|
# Fields are read from the global Constants module.
|
|
defp get_member_field_configurations do
|
|
# Get all eligible fields from the global constants
|
|
all_fields = Mv.Constants.member_fields()
|
|
|
|
Enum.reduce(all_fields, %{}, fn field, acc ->
|
|
show_in_overview = Mv.Membership.Member.show_in_overview?(field)
|
|
Map.put(acc, field, show_in_overview)
|
|
end)
|
|
end
|
|
|
|
# Gets the list of member fields that should be visible in the overview.
|
|
#
|
|
# Filters the member field configurations to return only fields with show_in_overview: true.
|
|
#
|
|
# Returns a list of atoms representing visible member field names.
|
|
defp get_visible_member_fields do
|
|
get_member_field_configurations()
|
|
|> Enum.filter(fn {_field, show_in_overview} -> show_in_overview end)
|
|
|> Enum.map(fn {field, _show_in_overview} -> field end)
|
|
end
|
|
end
|