diff --git a/Justfile b/Justfile
index 1874b67..b28dbdc 100644
--- a/Justfile
+++ b/Justfile
@@ -35,8 +35,8 @@ audit:
mix deps.audit
mix hex.audit
-test: install-dependencies start-database
- mix test
+test *args: install-dependencies start-database
+ mix test {{args}}
format:
mix format
diff --git a/lib/mv_web/live/components/sort_header_component.ex b/lib/mv_web/live/components/sort_header_component.ex
new file mode 100644
index 0000000..e69357e
--- /dev/null
+++ b/lib/mv_web/live/components/sort_header_component.ex
@@ -0,0 +1,61 @@
+defmodule MvWeb.Components.SortHeaderComponent do
+ @moduledoc """
+ Sort Header that can be used as column header and sorts a table:
+ Props:
+ - field: atom() # Ash‑Field for sorting
+ - label: string() # Column Heading (can be aan heex templyte)
+ - sort_field: atom() | nil # current sort-field from parent liveview
+ - sort_order: :asc | :desc | nil # current sorting order
+ """
+ use MvWeb, :live_component
+
+ @impl true
+ def update(assigns, socket) do
+ {:ok, assign(socket, assigns)}
+ end
+
+ # Check if we can add the aria-sort label directly to the daisyUI header
+ # aria-sort={aria_sort(@field, @sort_field, @sort_order)}
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+ """
+ end
+
+ @impl true
+ def handle_event("sort", %{"field" => field_str}, socket) do
+ send(self(), {:sort, field_str})
+ {:noreply, socket}
+ end
+
+ # -------------------------------------------------
+ # Hilfsfunktionen für ARIA‑Attribute & Icon‑SVG
+ # -------------------------------------------------
+ defp aria_sort(field, sort_field, dir) when field == sort_field do
+ case dir do
+ :asc -> gettext("ascending")
+ :desc -> gettext("descending")
+ end
+ end
+
+ defp aria_sort(_, _, _), do: gettext("Click to sort")
+end
diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex
index 0a9d129..7a0de39 100644
--- a/lib/mv_web/live/member_live/index.ex
+++ b/lib/mv_web/live/member_live/index.ex
@@ -2,27 +2,109 @@ defmodule MvWeb.MemberLive.Index do
use MvWeb, :live_view
import Ash.Expr
import Ash.Query
- import MvWeb.TableComponents
@impl true
- def mount(_params, _session, socket) do
- members = Ash.read!(Mv.Membership.Member)
- sorted = Enum.sort_by(members, & &1.first_name)
+ 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, [])
- {:ok,
- socket
- |> assign(:page_title, gettext("Members"))
- |> assign(:query, "")
- |> assign(:sort_field, :first_name)
- |> assign(:sort_order, :asc)
- |> assign(:members, sorted)
- |> assign(:selected_members, [])}
+ # We call handle params to use the query from the URL
+ {:noreply, socket} = handle_params(params, nil, socket)
+ {:ok, socket}
end
# -----------------------------------------------------------------
- # Receive messages from any toolbar component
+ # 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)
+
+ {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}"
+
+ # Update the SortHeader to
+ send_update(MvWeb.Components.SortHeaderComponent,
+ id: active_id,
+ sort_field: new_field,
+ sort_order: new_order
+ )
+
+ # Build the URL with queries
+ query_params = %{
+ "sort_field" => Atom.to_string(new_field),
+ "sort_order" => Atom.to_string(new_order)
+ }
+
+ # "/members" is the path you defined in router.ex
+ new_path = "/members?" <> URI.encode_query(query_params)
+
+ # Push the new URL
+ {:noreply,
+ push_patch(socket,
+ to: new_path,
+ # replace true
+ replace: true
+ )}
+ end
+
# Function to handle search
@impl true
def handle_info({:search_changed, q}, socket) do
@@ -42,74 +124,73 @@ defmodule MvWeb.MemberLive.Index do
end
# -----------------------------------------------------------------
- # Handle Events
+ # Handle Params from the URL
# -----------------------------------------------------------------
-
@impl true
- def handle_event("delete", %{"id" => id}, socket) do
- member = Ash.get!(Mv.Membership.Member, id)
- Ash.destroy!(member)
+ def handle_params(params, _url, socket) do
+ socket =
+ socket
+ |> maybe_update_sort(params)
+ |> load_members()
- {:noreply, stream_delete(socket, :members, member)}
+ {:noreply, socket}
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
+ # -------------------------------------------------------------
+ # FUNCTIONS
+ # -------------------------------------------------------------
+ # Load members eg based on a query for sorting
+ defp load_members(socket) 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
+ ])
+ |> maybe_sort(socket.assigns.sort_field, socket.assigns.sort_order)
- {:noreply, assign(socket, :selected_members, selected)}
+ members = Ash.read!(query)
+ assign(socket, :members, members)
end
- # Sorts the list of members according to a field, when you click on the column header
- @impl true
- def handle_event("sort", %{"field" => field_str}, socket) do
- members = socket.assigns.members
- field = String.to_existing_atom(field_str)
-
- new_order =
- if socket.assigns.sort_field == field do
- toggle_order(socket.assigns.sort_order)
- else
- :asc
- end
-
- sorted_members =
- members
- |> Enum.sort_by(&Map.get(&1, field), sort_fun(new_order))
-
- {:noreply,
- socket
- |> assign(:sort_field, field)
- |> assign(:sort_order, new_order)
- |> assign(:members, sorted_members)}
- 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
+ # -------------------------------------------------------------
+ # Helper Functions
+ # -------------------------------------------------------------
+ # Functions to toggle sorting order
defp toggle_order(:asc), do: :desc
defp toggle_order(:desc), do: :asc
- defp sort_fun(:asc), do: &<=/2
- defp sort_fun(:desc), do: &>=/2
+ 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), do: Ash.Query.sort(query, [{field, :asc}])
+ defp maybe_sort(query, field, :desc), do: Ash.Query.sort(query, [{field, :desc}])
+
+ # Function to maybe update the sort
+ defp maybe_update_sort(socket, %{"sort_field" => sf, "sort_order" => so}) do
+ field =
+ try do
+ String.to_existing_atom(sf)
+ rescue
+ ArgumentError -> socket.assigns.sort_field
+ end
+
+ order = if so in ["asc", "desc"], do: String.to_atom(so), else: socket.assigns.sort_order
+
+ socket
+ |> assign(:sort_field, field)
+ |> assign(:sort_order, order)
+ end
+
+ defp maybe_update_sort(socket, _), do: socket
end
diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex
index aa7a820..4072a51 100644
--- a/lib/mv_web/live/member_live/index.html.heex
+++ b/lib/mv_web/live/member_live/index.html.heex
@@ -52,23 +52,139 @@
<:col
:let={member}
label={
- sort_button(%{
- field: :first_name,
- label: gettext("Name"),
- sort_field: @sort_field,
- sort_order: @sort_order
- })
+ ~H"""
+ <.live_component
+ module={MvWeb.Components.SortHeaderComponent}
+ id={:sort_first_name}
+ field={:first_name}
+ label={gettext("First name")}
+ sort_field={@sort_field}
+ sort_order={@sort_order}
+ />
+ """
}
>
{member.first_name} {member.last_name}
- <:col :let={member} label={gettext("Email")}>{member.email}
- <:col :let={member} label={gettext("Street")}>{member.street}
- <:col :let={member} label={gettext("House Number")}>{member.house_number}
- <:col :let={member} label={gettext("Postal Code")}>{member.postal_code}
- <:col :let={member} label={gettext("City")}>{member.city}
- <:col :let={member} label={gettext("Phone Number")}>{member.phone_number}
- <:col :let={member} label={gettext("Join Date")}>{member.join_date}
+ <:col
+ :let={member}
+ label={
+ ~H"""
+ <.live_component
+ module={MvWeb.Components.SortHeaderComponent}
+ id={:sort_email}
+ field={:email}
+ label={gettext("Email")}
+ sort_field={@sort_field}
+ sort_order={@sort_order}
+ />
+ """
+ }
+ >
+ {member.email}
+
+ <:col
+ :let={member}
+ label={
+ ~H"""
+ <.live_component
+ module={MvWeb.Components.SortHeaderComponent}
+ id={:sort_street}
+ field={:street}
+ label={gettext("Street")}
+ sort_field={@sort_field}
+ sort_order={@sort_order}
+ />
+ """
+ }
+ >
+ {member.street}
+
+ <:col
+ :let={member}
+ label={
+ ~H"""
+ <.live_component
+ module={MvWeb.Components.SortHeaderComponent}
+ id={:sort_house_number}
+ field={:house_number}
+ label={gettext("House Number")}
+ sort_field={@sort_field}
+ sort_order={@sort_order}
+ />
+ """
+ }
+ >
+ {member.house_number}
+
+ <:col
+ :let={member}
+ label={
+ ~H"""
+ <.live_component
+ module={MvWeb.Components.SortHeaderComponent}
+ id={:sort_postal_code}
+ field={:postal_code}
+ label={gettext("Postal Code")}
+ sort_field={@sort_field}
+ sort_order={@sort_order}
+ />
+ """
+ }
+ >
+ {member.postal_code}
+
+ <:col
+ :let={member}
+ label={
+ ~H"""
+ <.live_component
+ module={MvWeb.Components.SortHeaderComponent}
+ id={:sort_city}
+ field={:city}
+ label={gettext("City")}
+ sort_field={@sort_field}
+ sort_order={@sort_order}
+ />
+ """
+ }
+ >
+ {member.city}
+
+ <:col
+ :let={member}
+ label={
+ ~H"""
+ <.live_component
+ module={MvWeb.Components.SortHeaderComponent}
+ id={:sort_phone_number}
+ field={:phone_number}
+ label={gettext("Phone Number")}
+ sort_field={@sort_field}
+ sort_order={@sort_order}
+ />
+ """
+ }
+ >
+ {member.phone_number}
+
+ <:col
+ :let={member}
+ label={
+ ~H"""
+ <.live_component
+ module={MvWeb.Components.SortHeaderComponent}
+ id={:sort_join_date}
+ field={:join_date}
+ label={gettext("Join Date")}
+ sort_field={@sort_field}
+ sort_order={@sort_order}
+ />
+ """
+ }
+ >
+ {member.join_date}
+
<:action :let={member}>
diff --git a/priv/gettext/de/LC_MESSAGES/auth.po b/priv/gettext/de/LC_MESSAGES/auth.po
index 0f2202d..661c269 100644
--- a/priv/gettext/de/LC_MESSAGES/auth.po
+++ b/priv/gettext/de/LC_MESSAGES/auth.po
@@ -62,5 +62,5 @@ msgstr "Anmelden..."
msgid "Your password has successfully been reset"
msgstr "Das Passwort wurde erfolgreich zurückgesetzt"
-msgid "Sign in with Rauthy"
-msgstr "Anmelden mit der Vereinscloud"
+#~ msgid "Sign in with Rauthy"
+#~ msgstr "Anmelden mit der Vereinscloud"
diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po
index 1a7cf7e..14ca2d4 100644
--- a/priv/gettext/de/LC_MESSAGES/default.po
+++ b/priv/gettext/de/LC_MESSAGES/default.po
@@ -15,7 +15,7 @@ msgstr ""
msgid "Actions"
msgstr "Aktionen"
-#: lib/mv_web/live/member_live/index.html.heex:77
+#: lib/mv_web/live/member_live/index.html.heex:193
#: lib/mv_web/live/user_live/index.html.heex:65
#, elixir-autogen, elixir-format
msgid "Are you sure?"
@@ -28,19 +28,19 @@ msgid "Attempting to reconnect"
msgstr "Verbindung wird wiederhergestellt"
#: lib/mv_web/live/member_live/form.ex:25
-#: lib/mv_web/live/member_live/index.html.heex:62
+#: lib/mv_web/live/member_live/index.html.heex:138
#: lib/mv_web/live/member_live/show.ex:36
#, elixir-autogen, elixir-format
msgid "City"
msgstr "Stadt"
-#: lib/mv_web/live/member_live/index.html.heex:79
+#: lib/mv_web/live/member_live/index.html.heex:195
#: lib/mv_web/live/user_live/index.html.heex:67
#, elixir-autogen, elixir-format
msgid "Delete"
msgstr "Löschen"
-#: lib/mv_web/live/member_live/index.html.heex:71
+#: lib/mv_web/live/member_live/index.html.heex:187
#: lib/mv_web/live/user_live/form.ex:109
#: lib/mv_web/live/user_live/index.html.heex:59
#, elixir-autogen, elixir-format
@@ -54,7 +54,7 @@ msgid "Edit Member"
msgstr "Mitglied bearbeiten"
#: lib/mv_web/live/member_live/form.ex:18
-#: lib/mv_web/live/member_live/index.html.heex:58
+#: lib/mv_web/live/member_live/index.html.heex:70
#: lib/mv_web/live/member_live/show.ex:27
#: lib/mv_web/live/user_live/form.ex:14
#: lib/mv_web/live/user_live/index.html.heex:44
@@ -70,7 +70,7 @@ msgid "First Name"
msgstr "Vorname"
#: lib/mv_web/live/member_live/form.ex:22
-#: lib/mv_web/live/member_live/index.html.heex:64
+#: lib/mv_web/live/member_live/index.html.heex:172
#: lib/mv_web/live/member_live/show.ex:33
#, elixir-autogen, elixir-format
msgid "Join Date"
@@ -87,7 +87,7 @@ msgstr "Nachname"
msgid "New Member"
msgstr "Neues Mitglied"
-#: lib/mv_web/live/member_live/index.html.heex:68
+#: lib/mv_web/live/member_live/index.html.heex:184
#: lib/mv_web/live/user_live/index.html.heex:56
#, elixir-autogen, elixir-format
msgid "Show"
@@ -127,7 +127,7 @@ msgid "Exit Date"
msgstr "Austrittsdatum"
#: lib/mv_web/live/member_live/form.ex:27
-#: lib/mv_web/live/member_live/index.html.heex:60
+#: lib/mv_web/live/member_live/index.html.heex:104
#: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format
msgid "House Number"
@@ -146,14 +146,14 @@ msgid "Paid"
msgstr "Bezahlt"
#: lib/mv_web/live/member_live/form.ex:21
-#: lib/mv_web/live/member_live/index.html.heex:63
+#: lib/mv_web/live/member_live/index.html.heex:155
#: lib/mv_web/live/member_live/show.ex:32
#, elixir-autogen, elixir-format
msgid "Phone Number"
msgstr "Telefonnummer"
#: lib/mv_web/live/member_live/form.ex:28
-#: lib/mv_web/live/member_live/index.html.heex:61
+#: lib/mv_web/live/member_live/index.html.heex:121
#: lib/mv_web/live/member_live/show.ex:39
#, elixir-autogen, elixir-format
msgid "Postal Code"
@@ -173,7 +173,7 @@ msgid "Saving..."
msgstr "Speichern..."
#: lib/mv_web/live/member_live/form.ex:26
-#: lib/mv_web/live/member_live/index.html.heex:59
+#: lib/mv_web/live/member_live/index.html.heex:87
#: lib/mv_web/live/member_live/show.ex:37
#, elixir-autogen, elixir-format
msgid "Street"
@@ -318,13 +318,12 @@ msgid "Member"
msgstr "Mitglied"
#: lib/mv_web/components/layouts/navbar.ex:14
-#: lib/mv_web/live/member_live/index.ex:12
+#: lib/mv_web/live/member_live/index.ex:8
#: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format
msgid "Members"
msgstr "Mitglieder"
-#: lib/mv_web/live/member_live/index.html.heex:50
#: lib/mv_web/live/property_type_live/form.ex:16
#, elixir-autogen, elixir-format
msgid "Name"
@@ -469,11 +468,13 @@ msgid "Value type"
msgstr "Wertetyp"
#: lib/mv_web/components/table_components.ex:30
+#: lib/mv_web/live/components/sort_header_component.ex:55
#, elixir-autogen, elixir-format
msgid "ascending"
msgstr "aufsteigend"
#: lib/mv_web/components/table_components.ex:30
+#: lib/mv_web/live/components/sort_header_component.ex:56
#, elixir-autogen, elixir-format
msgid "descending"
msgstr "absteigend"
@@ -553,7 +554,17 @@ msgstr "Passwort setzen"
msgid "User will be created without a password. Check 'Set Password' to add one."
msgstr "Benutzer wird ohne Passwort erstellt. Aktivieren Sie 'Passwort setzen', um eines hinzuzufügen."
-#: lib/mv_web/auth_overrides.ex:30
+#: lib/mv_web/live/components/sort_header_component.ex:60
#, elixir-autogen, elixir-format
-msgid "or"
-msgstr "oder"
+msgid "Click to sort"
+msgstr "Klicke um zu sortieren"
+
+#: lib/mv_web/live/member_live/index.html.heex:53
+#, elixir-autogen, elixir-format, fuzzy
+msgid "First name"
+msgstr "Vorname"
+
+#~ #: lib/mv_web/auth_overrides.ex:30
+#~ #, elixir-autogen, elixir-format
+#~ msgid "or"
+#~ msgstr "oder"
diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot
index a9bfb08..58a0182 100644
--- a/priv/gettext/default.pot
+++ b/priv/gettext/default.pot
@@ -16,7 +16,7 @@ msgstr ""
msgid "Actions"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:77
+#: lib/mv_web/live/member_live/index.html.heex:193
#: lib/mv_web/live/user_live/index.html.heex:65
#, elixir-autogen, elixir-format
msgid "Are you sure?"
@@ -29,19 +29,19 @@ msgid "Attempting to reconnect"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:25
-#: lib/mv_web/live/member_live/index.html.heex:62
+#: lib/mv_web/live/member_live/index.html.heex:138
#: lib/mv_web/live/member_live/show.ex:36
#, elixir-autogen, elixir-format
msgid "City"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:79
+#: lib/mv_web/live/member_live/index.html.heex:195
#: lib/mv_web/live/user_live/index.html.heex:67
#, elixir-autogen, elixir-format
msgid "Delete"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:71
+#: lib/mv_web/live/member_live/index.html.heex:187
#: lib/mv_web/live/user_live/form.ex:109
#: lib/mv_web/live/user_live/index.html.heex:59
#, elixir-autogen, elixir-format
@@ -55,7 +55,7 @@ msgid "Edit Member"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:18
-#: lib/mv_web/live/member_live/index.html.heex:58
+#: lib/mv_web/live/member_live/index.html.heex:70
#: lib/mv_web/live/member_live/show.ex:27
#: lib/mv_web/live/user_live/form.ex:14
#: lib/mv_web/live/user_live/index.html.heex:44
@@ -71,7 +71,7 @@ msgid "First Name"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:22
-#: lib/mv_web/live/member_live/index.html.heex:64
+#: lib/mv_web/live/member_live/index.html.heex:172
#: lib/mv_web/live/member_live/show.ex:33
#, elixir-autogen, elixir-format
msgid "Join Date"
@@ -88,7 +88,7 @@ msgstr ""
msgid "New Member"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:68
+#: lib/mv_web/live/member_live/index.html.heex:184
#: lib/mv_web/live/user_live/index.html.heex:56
#, elixir-autogen, elixir-format
msgid "Show"
@@ -128,7 +128,7 @@ msgid "Exit Date"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:27
-#: lib/mv_web/live/member_live/index.html.heex:60
+#: lib/mv_web/live/member_live/index.html.heex:104
#: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format
msgid "House Number"
@@ -147,14 +147,14 @@ msgid "Paid"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:21
-#: lib/mv_web/live/member_live/index.html.heex:63
+#: lib/mv_web/live/member_live/index.html.heex:155
#: lib/mv_web/live/member_live/show.ex:32
#, elixir-autogen, elixir-format
msgid "Phone Number"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:28
-#: lib/mv_web/live/member_live/index.html.heex:61
+#: lib/mv_web/live/member_live/index.html.heex:121
#: lib/mv_web/live/member_live/show.ex:39
#, elixir-autogen, elixir-format
msgid "Postal Code"
@@ -174,7 +174,7 @@ msgid "Saving..."
msgstr ""
#: lib/mv_web/live/member_live/form.ex:26
-#: lib/mv_web/live/member_live/index.html.heex:59
+#: lib/mv_web/live/member_live/index.html.heex:87
#: lib/mv_web/live/member_live/show.ex:37
#, elixir-autogen, elixir-format
msgid "Street"
@@ -319,13 +319,12 @@ msgid "Member"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:14
-#: lib/mv_web/live/member_live/index.ex:12
+#: lib/mv_web/live/member_live/index.ex:8
#: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format
msgid "Members"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:50
#: lib/mv_web/live/property_type_live/form.ex:16
#, elixir-autogen, elixir-format
msgid "Name"
@@ -470,11 +469,13 @@ msgid "Value type"
msgstr ""
#: lib/mv_web/components/table_components.ex:30
+#: lib/mv_web/live/components/sort_header_component.ex:55
#, elixir-autogen, elixir-format
msgid "ascending"
msgstr ""
#: lib/mv_web/components/table_components.ex:30
+#: lib/mv_web/live/components/sort_header_component.ex:56
#, elixir-autogen, elixir-format
msgid "descending"
msgstr ""
@@ -554,7 +555,12 @@ msgstr ""
msgid "User will be created without a password. Check 'Set Password' to add one."
msgstr ""
-#: lib/mv_web/auth_overrides.ex:30
+#: lib/mv_web/live/components/sort_header_component.ex:60
#, elixir-autogen, elixir-format
-msgid "or"
+msgid "Click to sort"
+msgstr ""
+
+#: lib/mv_web/live/member_live/index.html.heex:53
+#, elixir-autogen, elixir-format
+msgid "First name"
msgstr ""
diff --git a/priv/gettext/en/LC_MESSAGES/auth.po b/priv/gettext/en/LC_MESSAGES/auth.po
index 1e4e801..59ce742 100644
--- a/priv/gettext/en/LC_MESSAGES/auth.po
+++ b/priv/gettext/en/LC_MESSAGES/auth.po
@@ -59,5 +59,5 @@ msgstr ""
msgid "Your password has successfully been reset"
msgstr ""
-msgid "Sign in with Rauthy"
-msgstr "Sign in with Vereinscloud"
+#~ msgid "Sign in with Rauthy"
+#~ msgstr "Sign in with Vereinscloud"
diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po
index 2f09378..6311138 100644
--- a/priv/gettext/en/LC_MESSAGES/default.po
+++ b/priv/gettext/en/LC_MESSAGES/default.po
@@ -16,7 +16,7 @@ msgstr ""
msgid "Actions"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:77
+#: lib/mv_web/live/member_live/index.html.heex:193
#: lib/mv_web/live/user_live/index.html.heex:65
#, elixir-autogen, elixir-format
msgid "Are you sure?"
@@ -29,19 +29,19 @@ msgid "Attempting to reconnect"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:25
-#: lib/mv_web/live/member_live/index.html.heex:62
+#: lib/mv_web/live/member_live/index.html.heex:138
#: lib/mv_web/live/member_live/show.ex:36
#, elixir-autogen, elixir-format
msgid "City"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:79
+#: lib/mv_web/live/member_live/index.html.heex:195
#: lib/mv_web/live/user_live/index.html.heex:67
#, elixir-autogen, elixir-format
msgid "Delete"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:71
+#: lib/mv_web/live/member_live/index.html.heex:187
#: lib/mv_web/live/user_live/form.ex:109
#: lib/mv_web/live/user_live/index.html.heex:59
#, elixir-autogen, elixir-format
@@ -55,7 +55,7 @@ msgid "Edit Member"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:18
-#: lib/mv_web/live/member_live/index.html.heex:58
+#: lib/mv_web/live/member_live/index.html.heex:70
#: lib/mv_web/live/member_live/show.ex:27
#: lib/mv_web/live/user_live/form.ex:14
#: lib/mv_web/live/user_live/index.html.heex:44
@@ -71,7 +71,7 @@ msgid "First Name"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:22
-#: lib/mv_web/live/member_live/index.html.heex:64
+#: lib/mv_web/live/member_live/index.html.heex:172
#: lib/mv_web/live/member_live/show.ex:33
#, elixir-autogen, elixir-format
msgid "Join Date"
@@ -88,7 +88,7 @@ msgstr ""
msgid "New Member"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:68
+#: lib/mv_web/live/member_live/index.html.heex:184
#: lib/mv_web/live/user_live/index.html.heex:56
#, elixir-autogen, elixir-format
msgid "Show"
@@ -128,7 +128,7 @@ msgid "Exit Date"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:27
-#: lib/mv_web/live/member_live/index.html.heex:60
+#: lib/mv_web/live/member_live/index.html.heex:104
#: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format
msgid "House Number"
@@ -147,14 +147,14 @@ msgid "Paid"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:21
-#: lib/mv_web/live/member_live/index.html.heex:63
+#: lib/mv_web/live/member_live/index.html.heex:155
#: lib/mv_web/live/member_live/show.ex:32
#, elixir-autogen, elixir-format
msgid "Phone Number"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:28
-#: lib/mv_web/live/member_live/index.html.heex:61
+#: lib/mv_web/live/member_live/index.html.heex:121
#: lib/mv_web/live/member_live/show.ex:39
#, elixir-autogen, elixir-format
msgid "Postal Code"
@@ -174,7 +174,7 @@ msgid "Saving..."
msgstr ""
#: lib/mv_web/live/member_live/form.ex:26
-#: lib/mv_web/live/member_live/index.html.heex:59
+#: lib/mv_web/live/member_live/index.html.heex:87
#: lib/mv_web/live/member_live/show.ex:37
#, elixir-autogen, elixir-format
msgid "Street"
@@ -319,13 +319,12 @@ msgid "Member"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:14
-#: lib/mv_web/live/member_live/index.ex:12
+#: lib/mv_web/live/member_live/index.ex:8
#: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format
msgid "Members"
msgstr ""
-#: lib/mv_web/live/member_live/index.html.heex:50
#: lib/mv_web/live/property_type_live/form.ex:16
#, elixir-autogen, elixir-format
msgid "Name"
@@ -470,11 +469,13 @@ msgid "Value type"
msgstr ""
#: lib/mv_web/components/table_components.ex:30
+#: lib/mv_web/live/components/sort_header_component.ex:55
#, elixir-autogen, elixir-format
msgid "ascending"
msgstr ""
#: lib/mv_web/components/table_components.ex:30
+#: lib/mv_web/live/components/sort_header_component.ex:56
#, elixir-autogen, elixir-format
msgid "descending"
msgstr ""
@@ -554,7 +555,17 @@ msgstr "Set Password"
msgid "User will be created without a password. Check 'Set Password' to add one."
msgstr "User will be created without a password. Check 'Set Password' to add one."
-#: lib/mv_web/auth_overrides.ex:30
+#: lib/mv_web/live/components/sort_header_component.ex:60
#, elixir-autogen, elixir-format
-msgid "or"
+msgid "Click to sort"
msgstr ""
+
+#: lib/mv_web/live/member_live/index.html.heex:53
+#, elixir-autogen, elixir-format, fuzzy
+msgid "First name"
+msgstr ""
+
+#~ #: lib/mv_web/auth_overrides.ex:30
+#~ #, elixir-autogen, elixir-format
+#~ msgid "or"
+#~ msgstr ""
diff --git a/test/mv_web/components/sort_header_component_test.exs b/test/mv_web/components/sort_header_component_test.exs
new file mode 100644
index 0000000..9b1c006
--- /dev/null
+++ b/test/mv_web/components/sort_header_component_test.exs
@@ -0,0 +1,12 @@
+defmodule MvWeb.Components.SortHeaderComponentTest do
+ use MvWeb.ConnCase, async: true
+ use Phoenix.Component
+ import Phoenix.LiveViewTest
+
+ test "renders sort header with correct attributes", %{conn: conn} do
+ conn = conn_with_oidc_user(conn)
+ {:ok, view, _html} = live(conn, "/members")
+
+ assert view |> element("[data-testid='first_name']")
+ end
+end
diff --git a/test/mv_web/member_live/index_test.exs b/test/mv_web/member_live/index_test.exs
index b8b573c..f697d6e 100644
--- a/test/mv_web/member_live/index_test.exs
+++ b/test/mv_web/member_live/index_test.exs
@@ -1,6 +1,7 @@
defmodule MvWeb.MemberLive.IndexTest do
use MvWeb.ConnCase, async: true
import Phoenix.LiveViewTest
+ require Ash.Query
test "shows translated title in German", %{conn: conn} do
conn = conn_with_oidc_user(conn)
@@ -55,7 +56,6 @@ defmodule MvWeb.MemberLive.IndexTest do
test "shows translated flash message after creating a member in English", %{conn: conn} do
conn = conn_with_oidc_user(conn)
- conn = Plug.Test.init_test_session(conn, locale: "en")
{:ok, form_view, _html} = live(conn, "/members/new")
form_data = %{
@@ -74,6 +74,42 @@ defmodule MvWeb.MemberLive.IndexTest do
assert has_element?(index_view, "#flash-group", "Member create successfully")
end
+ describe "sorting interaction" do
+ test "clicking a column header toggles sort order and updates the URL", %{conn: conn} do
+ conn = conn_with_oidc_user(conn)
+ {:ok, view, _html} = live(conn, "/members")
+
+ # The component data test ids are built as ""
+ # First click – should sort ASC
+ view
+ |> element("[data-testid='email']")
+ |> render_click()
+
+ # The LiveView pushes a patch with the new query params
+ assert_patch(view, "/members?sort_field=email&sort_order=asc")
+
+ # Second click – toggles to DESC
+ view
+ |> element("[data-testid='email']")
+ |> render_click()
+
+ assert_patch(view, "/members?sort_field=email&sort_order=desc")
+ end
+ end
+
+ describe "URL param handling" do
+ test "handle_params reads sort query and applies it", %{conn: conn} do
+ conn = conn_with_oidc_user(conn)
+ url = "/members?sort_field=email&sort_order=desc"
+
+ conn = get(conn, url)
+
+ # The LiveView must have parsed the params and stored them as atoms.
+ assert conn.assigns.sort_field == :email
+ assert conn.assigns.sort_order == :desc
+ end
+ end
+
test "handle_info(:search_changed) updates assigns with search results", %{conn: conn} do
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members")
@@ -85,4 +121,31 @@ defmodule MvWeb.MemberLive.IndexTest do
assert state.socket.assigns.query == "Friedrich"
assert is_list(state.socket.assigns.members)
end
+
+ test "can delete a member without error", %{conn: conn} do
+ # Create a test member first
+ {:ok, member} =
+ Mv.Membership.create_member(%{
+ first_name: "Test",
+ last_name: "User",
+ email: "test@example.com"
+ })
+
+ conn = conn_with_oidc_user(conn)
+ {:ok, index_view, _html} = live(conn, "/members")
+
+ # Verify the member is displayed
+ assert has_element?(index_view, "#members", "Test User")
+
+ # Click the delete link for this member
+ index_view
+ |> element("a", "Delete")
+ |> render_click()
+
+ # Verify the member is no longer displayed
+ refute has_element?(index_view, "#members", "Test User")
+
+ # Verify the member was actually deleted from the database
+ assert not (Mv.Membership.Member |> Ash.Query.filter(id == ^member.id) |> Ash.exists?())
+ end
end