defmodule MvWeb.MemberLive.Index do use MvWeb, :live_view import Ash.Expr import Ash.Query @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 |> filter(expr(fragment("search_vector @@ plainto_tsquery('simple', ?)", ^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