finalize groups #437

Merged
simon merged 4 commits from feature/finalize-groups into main 2026-02-23 17:27:49 +01:00
5 changed files with 93 additions and 85 deletions
Showing only changes of commit 83b104ecf3 - Show all commits

View file

@ -1264,6 +1264,8 @@ end
### 3.12 Internationalization: Gettext
**German (de):** Use informal address (“duzen”). All user-facing German text should address the user as “du” (e.g. “Bitte versuche es erneut”, “Deine Einstellungen”), not “Sie”.
**Define Translations:**
```elixir

View file

@ -17,10 +17,12 @@ defmodule MvWeb.GroupLive.Show do
require Logger
import Ash.Expr
import MvWeb.LiveHelpers, only: [current_actor: 1]
import MvWeb.Authorization
alias Mv.Membership
alias MvWeb.Helpers.MemberHelpers, as: MemberHelpers
@impl true
def mount(_params, _session, socket) do
@ -29,6 +31,7 @@ defmodule MvWeb.GroupLive.Show do
|> assign(:show_add_member_input, false)
|> assign(:member_search_query, "")
|> assign(:available_members, [])
|> assign(:add_member_candidates, [])
|> assign(:selected_member_ids, [])
|> assign(:selected_members, [])
|> assign(:show_member_dropdown, false)
@ -94,12 +97,12 @@ defmodule MvWeb.GroupLive.Show do
</h1>
<div class="flex gap-2">
<%= if can?(@current_user, :update, Mv.Membership.Group) do %>
<%= if can?(@current_user, :update, @group) do %>
<.button variant="primary" navigate={~p"/groups/#{@group.slug}/edit"}>
{gettext("Edit")}
</.button>
<% end %>
<%= if can?(@current_user, :destroy, Mv.Membership.Group) do %>
<%= if can?(@current_user, :destroy, @group) do %>
<.button class="btn-error" phx-click="open_delete_modal">
{gettext("Delete")}
</.button>
@ -132,7 +135,7 @@ defmodule MvWeb.GroupLive.Show do
)}
</p>
<%= if can?(@current_user, :update, Mv.Membership.Group) do %>
<%= if can?(@current_user, :update, @group) do %>
<div class="mb-4">
<%= if assigns[:show_add_member_input] do %>
<div class="join w-full">
@ -263,7 +266,7 @@ defmodule MvWeb.GroupLive.Show do
<tr>
<th>{gettext("Name")}</th>
<th>{gettext("Email")}</th>
<%= if can?(@current_user, :update, Mv.Membership.Group) do %>
<%= if can?(@current_user, :update, @group) do %>
<th class="w-0">{gettext("Actions")}</th>
<% end %>
</tr>
@ -291,7 +294,7 @@ defmodule MvWeb.GroupLive.Show do
<span class="text-base-content/50 italic"></span>
<% end %>
</td>
<%= if can?(@current_user, :update, Mv.Membership.Group) do %>
<%= if can?(@current_user, :update, @group) do %>
<td>
<button
type="button"
@ -431,24 +434,31 @@ defmodule MvWeb.GroupLive.Show do
# Add Member Events
@impl true
def handle_event("show_add_member_input", _params, socket) do
# Use existing @group from assigns; no DB read on focus. Reload only on commit (add/remove).
{:noreply,
socket
|> assign(:show_add_member_input, true)
|> assign(:member_search_query, "")
|> assign(:available_members, [])
|> assign(:selected_member_ids, [])
|> assign(:selected_members, [])
|> assign(:show_member_dropdown, false)
|> assign(:focused_member_index, nil)}
# Load candidate members once (single DB read). Search/focus then filter in memory (R2).
socket =
socket
|> assign(:show_add_member_input, true)
|> assign(:member_search_query, "")
|> assign(:selected_member_ids, [])
|> assign(:selected_members, [])
|> assign(:show_member_dropdown, false)
|> assign(:focused_member_index, nil)
|> load_add_member_candidates()
{:noreply, socket}
end
@impl true
def handle_event("show_member_dropdown", _params, socket) do
# Use existing group.members for filtering; reload only on add/remove
# Filter in memory from preloaded candidates; no DB read (R2).
query = socket.assigns.member_search_query || ""
socket =
socket
|> load_available_members("")
|> assign(
:available_members,
filter_candidates_in_memory(socket.assigns.add_member_candidates, query)
)
|> assign(:show_member_dropdown, true)
|> assign(:focused_member_index, nil)
@ -462,6 +472,7 @@ defmodule MvWeb.GroupLive.Show do
|> assign(:show_add_member_input, false)
|> assign(:member_search_query, "")
|> assign(:available_members, [])
|> assign(:add_member_candidates, [])
|> assign(:selected_member_ids, [])
|> assign(:selected_members, [])
|> assign(:show_member_dropdown, false)
@ -528,11 +539,13 @@ defmodule MvWeb.GroupLive.Show do
@impl true
def handle_event("search_members", %{"member_search" => query}, socket) do
# Use existing group.members for filtering; reload only on add/remove
# Filter in memory from preloaded candidates; no DB read (R2).
candidates = socket.assigns.add_member_candidates || []
socket =
socket
|> assign(:member_search_query, query)
|> load_available_members(query)
|> assign(:available_members, filter_candidates_in_memory(candidates, query))
|> assign(:show_member_dropdown, true)
|> assign(:focused_member_index, nil)
@ -656,47 +669,69 @@ defmodule MvWeb.GroupLive.Show do
end
end
defp load_available_members(socket, query) do
# Load candidate members once when opening add-member UI (single DB read).
defp load_add_member_candidates(socket) do
require Ash.Query
current_member_ids = group_member_ids_set(socket.assigns.group)
base_query = available_members_base_query(query)
# Fetch more than 10, then exclude already-in-group and take 10 (avoids empty dropdown when first N are all in group)
fetch_limit = 50
limited_query = Ash.Query.limit(base_query, fetch_limit)
group = socket.assigns.group
exclude_ids = group_member_ids_set(group) |> MapSet.to_list()
actor = current_actor(socket)
case Ash.read(limited_query, actor: actor, domain: Mv.Membership) do
{:ok, members} ->
available =
members
|> Enum.reject(fn m -> MapSet.member?(current_member_ids, m.id) end)
|> Enum.take(10)
if exclude_ids == [] do
# No members in group; load first N members
query =
Mv.Membership.Member
|> Ash.Query.sort([:last_name, :first_name])
|> Ash.Query.limit(300)
assign(socket, available_members: available)
do_load_add_member_candidates(socket, query, actor)
else
query =
Mv.Membership.Member
|> Ash.Query.filter(expr(id not in ^exclude_ids))
|> Ash.Query.sort([:last_name, :first_name])
|> Ash.Query.limit(300)
do_load_add_member_candidates(socket, query, actor)
end
end
defp do_load_add_member_candidates(socket, query, actor) do
case Ash.read(query, actor: actor, domain: Mv.Membership) do
{:ok, candidates} ->
socket
|> assign(:add_member_candidates, candidates)
|> assign(:available_members, Enum.take(candidates, 10))
{:error, error} ->
Logger.warning("Failed to load available members for group: #{inspect(error)}")
Logger.warning("Failed to load add-member candidates: #{inspect(error)}")
socket
|> put_flash(:error, gettext("Could not load member search. Please try again."))
|> put_flash(:error, gettext("Could not load member list. Please try again."))
|> assign(:add_member_candidates, [])
|> assign(:available_members, [])
end
end
defp available_members_base_query(query) do
search_query = if query && String.trim(query) != "", do: String.trim(query), else: nil
# Filter preloaded candidates by query string (name/email). No DB read. R2.
defp filter_candidates_in_memory(candidates, query) when is_list(candidates) do
q = if is_binary(query), do: String.trim(query) |> String.downcase(), else: ""
if search_query do
Mv.Membership.Member
|> Ash.Query.for_read(:search, %{query: search_query})
if q == "" do
candidates |> Enum.take(10)
else
Mv.Membership.Member
|> Ash.Query.new()
candidates
|> Enum.filter(fn m ->
name = MemberHelpers.display_name(m) |> String.downcase()
email = (m.email || "") |> String.downcase()
String.contains?(name, q) or String.contains?(email, q)
end)
|> Enum.take(10)
end
end
defp filter_candidates_in_memory(_, _), do: []
defp group_member_ids_set(group) do
members = group.members || []
members |> Enum.map(& &1.id) |> MapSet.new()
@ -736,6 +771,7 @@ defmodule MvWeb.GroupLive.Show do
|> assign(:show_add_member_input, false)
|> assign(:member_search_query, "")
|> assign(:available_members, [])
|> assign(:add_member_candidates, [])
|> assign(:selected_member_ids, [])
|> assign(:selected_members, [])
|> assign(:show_member_dropdown, false)

View file

@ -2264,11 +2264,6 @@ msgstr "Nicht berechtigt."
msgid "Could not load data fields. Please check your permissions."
msgstr "Datenfelder konnten nicht geladen werden. Bitte überprüfe deine Berechtigungen."
#: lib/mv_web/live/group_live/show.ex
#, elixir-autogen, elixir-format
msgid "Could not load member search. Please try again."
msgstr "Mitgliedersuche konnte nicht geladen werden. Bitte versuchen Sie es erneut."
#: lib/mv_web/live/group_live/show.ex
#, elixir-autogen, elixir-format
msgid "Add Member"
@ -2609,17 +2604,7 @@ msgstr "Import"
msgid "Value type cannot be changed after creation"
msgstr "Der Wertetyp kann nach dem Erstellen nicht mehr geändert werden."
#~ #: lib/mv_web/live/import_export_live.ex
#~ #, elixir-autogen, elixir-format, fuzzy
#~ msgid "Export Members (CSV)"
#~ msgstr "Mitglieder exportieren (CSV)"
#~ #: lib/mv_web/live/import_export_live.ex
#~ #, elixir-autogen, elixir-format
#~ msgid "Export functionality will be available in a future release."
#~ msgstr "Export-Funktionalität ist im nächsten release verfügbar."
#~ #: lib/mv_web/live/import_export_live.ex
#~ #, elixir-autogen, elixir-format
#~ msgid "Import members from CSV files or export member data."
#~ msgstr "Importiere Mitglieder aus CSV-Dateien oder exportiere Mitgliederdaten."
#: lib/mv_web/live/group_live/show.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Could not load member list. Please try again."
msgstr "Mitgliederliste konnte nicht geladen werden. Bitte versuche es erneut."

View file

@ -2265,11 +2265,6 @@ msgstr ""
msgid "Could not load data fields. Please check your permissions."
msgstr ""
#: lib/mv_web/live/group_live/show.ex
#, elixir-autogen, elixir-format
msgid "Could not load member search. Please try again."
msgstr ""
#: lib/mv_web/live/group_live/show.ex
#, elixir-autogen, elixir-format
msgid "Add Member"
@ -2609,3 +2604,8 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "Value type cannot be changed after creation"
msgstr ""
#: lib/mv_web/live/group_live/show.ex
#, elixir-autogen, elixir-format
msgid "Could not load member list. Please try again."
msgstr ""

View file

@ -2265,11 +2265,6 @@ msgstr ""
msgid "Could not load data fields. Please check your permissions."
msgstr ""
#: lib/mv_web/live/group_live/show.ex
#, elixir-autogen, elixir-format
msgid "Could not load member search. Please try again."
msgstr ""
#: lib/mv_web/live/group_live/show.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Add Member"
@ -2610,17 +2605,7 @@ msgstr ""
msgid "Value type cannot be changed after creation"
msgstr ""
#~ #: lib/mv_web/live/import_export_live.ex
#~ #, elixir-autogen, elixir-format, fuzzy
#~ msgid "Export Members (CSV)"
#~ msgstr ""
#~ #: lib/mv_web/live/import_export_live.ex
#~ #, elixir-autogen, elixir-format
#~ msgid "Export functionality will be available in a future release."
#~ msgstr ""
#~ #: lib/mv_web/live/import_export_live.ex
#~ #, elixir-autogen, elixir-format
#~ msgid "Import members from CSV files or export member data."
#~ msgstr ""
#: lib/mv_web/live/group_live/show.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Could not load member list. Please try again."
msgstr ""