diff --git a/docs/groups-architecture.md b/docs/groups-architecture.md
index 27d9d18..1557b6f 100644
--- a/docs/groups-architecture.md
+++ b/docs/groups-architecture.md
@@ -961,6 +961,8 @@ Each functional unit can be implemented as a **separate issue**:
### Issue 4: Member Detail - Groups Display
**Type:** Frontend
**Estimation:** 1-2h
+**Status:** Implemented (Groups as data field in Personal Data, below Linked User; button-style links to `/groups/:slug`).
+
**Tasks:**
- Add groups section to member show
- Display group badges
diff --git a/lib/mv_web/live/member_live/show.ex b/lib/mv_web/live/member_live/show.ex
index 40491cc..cc0b25e 100644
--- a/lib/mv_web/live/member_live/show.ex
+++ b/lib/mv_web/live/member_live/show.ex
@@ -12,6 +12,8 @@ defmodule MvWeb.MemberLive.Show do
## Sections
- Personal Data: Name, address, contact information, membership dates, notes
- Custom Fields: Dynamic fields in uniform grid layout (sorted by name)
+ - Groups: Group links (buttons) in Personal Data section, below Linked User
+ - Payment Data: Membership fee type and cycle status
- Membership Fees: Tab showing all membership fee cycles with status management (via MembershipFeesComponent)
## Navigation
@@ -146,6 +148,28 @@ defmodule MvWeb.MemberLive.Show do
<% end %>
+ <%!-- Groups (in Personal Data, below Linked User) --%>
+
+ <.data_field label={gettext("Groups")}>
+ <%= if Enum.any?(@member.groups || []) do %>
+
+ <%= for group <- (@member.groups || []) do %>
+ <.link
+ navigate={~p"/groups/#{group.slug}"}
+ class="btn btn-xs btn-outline btn-primary"
+ role="status"
+ aria-label={gettext("Member of group %{name}", name: group.name)}
+ >
+ {group.name}
+
+ <% end %>
+
+ <% else %>
+
{gettext("No groups")}
+ <% end %>
+
+
+
<%!-- Notes --%>
<%= if @member.notes && String.trim(@member.notes) != "" do %>
@@ -262,7 +286,8 @@ defmodule MvWeb.MemberLive.Show do
:user,
:membership_fee_type,
custom_field_values: [:custom_field],
- membership_fee_cycles: [:membership_fee_type]
+ membership_fee_cycles: [:membership_fee_type],
+ groups: [:id, :name, :slug]
])
member = Ash.read_one!(query, actor: actor)
diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po
index 1784d4b..57334d2 100644
--- a/priv/gettext/de/LC_MESSAGES/default.po
+++ b/priv/gettext/de/LC_MESSAGES/default.po
@@ -2202,11 +2202,13 @@ msgstr "Gruppe erfolgreich gespeichert."
#: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex
+#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format
msgid "Groups"
msgstr "Gruppen"
#: lib/mv_web/live/group_live/index.ex
+#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format
msgid "No groups"
msgstr "Keine Gruppen"
@@ -2474,6 +2476,7 @@ msgid "unpaid"
msgstr "Unbezahlt"
#: lib/mv_web/live/member_live/index.html.heex
+#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format
msgid "Member of group %{name}"
msgstr "Mitglied der Gruppe %{name}"
diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot
index af24afd..fb156df 100644
--- a/priv/gettext/default.pot
+++ b/priv/gettext/default.pot
@@ -2203,11 +2203,13 @@ msgstr ""
#: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex
+#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format
msgid "Groups"
msgstr ""
#: lib/mv_web/live/group_live/index.ex
+#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format
msgid "No groups"
msgstr ""
@@ -2475,6 +2477,7 @@ msgid "unpaid"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex
+#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format
msgid "Member of group %{name}"
msgstr ""
diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po
index 88da6ff..1bf276c 100644
--- a/priv/gettext/en/LC_MESSAGES/default.po
+++ b/priv/gettext/en/LC_MESSAGES/default.po
@@ -2203,11 +2203,13 @@ msgstr ""
#: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex
+#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format
msgid "Groups"
msgstr ""
#: lib/mv_web/live/group_live/index.ex
+#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format
msgid "No groups"
msgstr ""
@@ -2475,6 +2477,7 @@ msgid "unpaid"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex
+#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format
msgid "Member of group %{name}"
msgstr ""
diff --git a/test/mv_web/member_live/show_groups_display_test.exs b/test/mv_web/member_live/show_groups_display_test.exs
index 78112ae..bda46b3 100644
--- a/test/mv_web/member_live/show_groups_display_test.exs
+++ b/test/mv_web/member_live/show_groups_display_test.exs
@@ -3,19 +3,15 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do
Tests for displaying groups in the member detail view (Issue #374).
Tests cover:
- - Groups section visibility (with and without groups)
- - Group badges with correct names and links to group detail pages
+ - Groups in Personal Data (with and without groups)
+ - Group buttons with correct names and links to group detail pages
- Edge cases (one group, many groups)
- - Security: groups section visible only when user may view member
- - Accessibility: badges have role and aria-label
+ - Security: groups visible only when user may view member
+ - Accessibility: group links have role and aria-label
## Note on async
async: false to avoid PostgreSQL deadlocks when creating members and groups
in the same test run (same as IndexGroupsDisplayTest).
-
- ## Expected state
- These tests fail until the Groups section is implemented on the member show page
- (Issue #374: load groups in handle_params, add "Groups" section with badges and links).
"""
use MvWeb.ConnCase, async: false
import Phoenix.LiveViewTest
@@ -97,10 +93,11 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do
member: member
} do
conn = conn_with_oidc_user(conn)
- {:ok, view, _html} = live(conn, ~p"/members/#{member}")
+ {:ok, _view, html} = live(conn, ~p"/members/#{member}")
- # Assert Groups section in main content (section with h2 "Groups"), not sidebar link
- assert has_element?(view, "main section h2", gettext("Groups"))
+ # Groups are in Personal Data; label "Groups" and empty state "No groups" must be present
+ assert html =~ gettext("Groups")
+ assert html =~ gettext("No groups")
end
test "groups are loaded with member (single request returns all group names)", %{