Add comprehensive module documentation to 12 LiveView modules covering member, user, property, and property_type management views.
320 lines
8.4 KiB
Elixir
320 lines
8.4 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
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
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, [])
|
|
|
|
# We call handle params to use the query from the URL
|
|
{:ok, socket}
|
|
end
|
|
|
|
# -----------------------------------------------------------------
|
|
# Handle Events
|
|
# -----------------------------------------------------------------
|
|
|
|
# Delete a member
|
|
@impl true
|
|
def handle_event("delete", %{"id" => id}, socket) do
|
|
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
|
|
|
|
# Selects one member in the list of members
|
|
@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
|
|
|
|
# Selects all members in the list of members
|
|
@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
|
|
# -----------------------------------------------------------------
|
|
|
|
# Sorts the list of members according to a field, when you click on the column header
|
|
@impl true
|
|
def handle_info({:sort, field_str}, socket) do
|
|
field = String.to_existing_atom(field_str)
|
|
old_field = socket.assigns.sort_field
|
|
|
|
{new_order, new_field} =
|
|
if socket.assigns.sort_field == field do
|
|
{toggle_order(socket.assigns.sort_order), field}
|
|
else
|
|
{:asc, field}
|
|
end
|
|
|
|
active_id = :"sort_#{new_field}"
|
|
old_id = :"sort_#{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
|
|
)
|
|
|
|
existing_search_query = socket.assigns.query
|
|
|
|
# Build the URL with queries
|
|
query_params = %{
|
|
"query" => existing_search_query,
|
|
"sort_field" => Atom.to_string(new_field),
|
|
"sort_order" => Atom.to_string(new_order)
|
|
}
|
|
|
|
# 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
|
|
|
|
# Function to handle search
|
|
@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
|
|
# -----------------------------------------------------------------
|
|
@impl true
|
|
def handle_params(params, _url, socket) do
|
|
socket =
|
|
socket
|
|
|> maybe_update_search(params)
|
|
|> maybe_update_sort(params)
|
|
|> load_members(params["query"])
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
# -------------------------------------------------------------
|
|
# FUNCTIONS
|
|
# -------------------------------------------------------------
|
|
# Load members eg based on a query for sorting
|
|
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
|
|
])
|
|
|
|
# Apply the search filter first
|
|
query = apply_search_filter(query, search_query)
|
|
|
|
# Apply sorting based on current socket state
|
|
query = maybe_sort(query, socket.assigns.sort_field, socket.assigns.sort_order)
|
|
|
|
members = Ash.read!(query)
|
|
assign(socket, :members, members)
|
|
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
|
|
defp maybe_sort(query, nil, _), do: query
|
|
|
|
defp maybe_sort(query, field, :asc) when not is_nil(field),
|
|
do: Ash.Query.sort(query, [{field, :asc}])
|
|
|
|
defp maybe_sort(query, field, :desc) when not is_nil(field),
|
|
do: Ash.Query.sort(query, [{field, :desc}])
|
|
|
|
defp maybe_sort(query, _, _), do: query
|
|
|
|
# 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
|
|
end
|
|
|
|
defp valid_sort_field?(_), do: false
|
|
|
|
# Function to maybe update the sort
|
|
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
|
|
|
|
defp determine_field(default, sf) do
|
|
case sf do
|
|
"" ->
|
|
default
|
|
|
|
nil ->
|
|
default
|
|
|
|
sf when is_binary(sf) ->
|
|
sf
|
|
|> String.to_existing_atom()
|
|
|> handle_atom_conversion(default)
|
|
|
|
sf when is_atom(sf) ->
|
|
handle_atom_conversion(sf, default)
|
|
|
|
_ ->
|
|
default
|
|
end
|
|
end
|
|
|
|
defp handle_atom_conversion(val, default) when is_atom(val) do
|
|
if valid_sort_field?(val), do: val, else: default
|
|
end
|
|
|
|
defp handle_atom_conversion(_, default), do: default
|
|
|
|
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
|
|
end
|