feat: add groups to member detail view #374
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Simon 2026-02-17 14:15:43 +01:00
parent 46f9094e1f
commit b1a9eb8b1d
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
6 changed files with 45 additions and 12 deletions

View file

@ -961,6 +961,8 @@ Each functional unit can be implemented as a **separate issue**:
### Issue 4: Member Detail - Groups Display ### Issue 4: Member Detail - Groups Display
**Type:** Frontend **Type:** Frontend
**Estimation:** 1-2h **Estimation:** 1-2h
**Status:** Implemented (Groups as data field in Personal Data, below Linked User; button-style links to `/groups/:slug`).
**Tasks:** **Tasks:**
- Add groups section to member show - Add groups section to member show
- Display group badges - Display group badges

View file

@ -12,6 +12,8 @@ defmodule MvWeb.MemberLive.Show do
## Sections ## Sections
- Personal Data: Name, address, contact information, membership dates, notes - Personal Data: Name, address, contact information, membership dates, notes
- Custom Fields: Dynamic fields in uniform grid layout (sorted by name) - 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) - Membership Fees: Tab showing all membership fee cycles with status management (via MembershipFeesComponent)
## Navigation ## Navigation
@ -146,6 +148,28 @@ defmodule MvWeb.MemberLive.Show do
</div> </div>
<% end %> <% end %>
<%!-- Groups (in Personal Data, below Linked User) --%>
<div>
<.data_field label={gettext("Groups")}>
<%= if Enum.any?(@member.groups || []) do %>
<div class="flex flex-wrap gap-2">
<%= 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}
</.link>
<% end %>
</div>
<% else %>
<span class="text-base-content/70 italic">{gettext("No groups")}</span>
<% end %>
</.data_field>
</div>
<%!-- Notes --%> <%!-- Notes --%>
<%= if @member.notes && String.trim(@member.notes) != "" do %> <%= if @member.notes && String.trim(@member.notes) != "" do %>
<div> <div>
@ -262,7 +286,8 @@ defmodule MvWeb.MemberLive.Show do
:user, :user,
:membership_fee_type, :membership_fee_type,
custom_field_values: [:custom_field], 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) member = Ash.read_one!(query, actor: actor)

View file

@ -2202,11 +2202,13 @@ msgstr "Gruppe erfolgreich gespeichert."
#: lib/mv_web/live/components/member_filter_component.ex #: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Groups" msgid "Groups"
msgstr "Gruppen" msgstr "Gruppen"
#: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No groups" msgid "No groups"
msgstr "Keine Gruppen" msgstr "Keine Gruppen"
@ -2474,6 +2476,7 @@ msgid "unpaid"
msgstr "Unbezahlt" msgstr "Unbezahlt"
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Member of group %{name}" msgid "Member of group %{name}"
msgstr "Mitglied der Gruppe %{name}" msgstr "Mitglied der Gruppe %{name}"

View file

@ -2203,11 +2203,13 @@ msgstr ""
#: lib/mv_web/live/components/member_filter_component.ex #: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Groups" msgid "Groups"
msgstr "" msgstr ""
#: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No groups" msgid "No groups"
msgstr "" msgstr ""
@ -2475,6 +2477,7 @@ msgid "unpaid"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Member of group %{name}" msgid "Member of group %{name}"
msgstr "" msgstr ""

View file

@ -2203,11 +2203,13 @@ msgstr ""
#: lib/mv_web/live/components/member_filter_component.ex #: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Groups" msgid "Groups"
msgstr "" msgstr ""
#: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No groups" msgid "No groups"
msgstr "" msgstr ""
@ -2475,6 +2477,7 @@ msgid "unpaid"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Member of group %{name}" msgid "Member of group %{name}"
msgstr "" msgstr ""

View file

@ -3,19 +3,15 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do
Tests for displaying groups in the member detail view (Issue #374). Tests for displaying groups in the member detail view (Issue #374).
Tests cover: Tests cover:
- Groups section visibility (with and without groups) - Groups in Personal Data (with and without groups)
- Group badges with correct names and links to group detail pages - Group buttons with correct names and links to group detail pages
- Edge cases (one group, many groups) - Edge cases (one group, many groups)
- Security: groups section visible only when user may view member - Security: groups visible only when user may view member
- Accessibility: badges have role and aria-label - Accessibility: group links have role and aria-label
## Note on async ## Note on async
async: false to avoid PostgreSQL deadlocks when creating members and groups async: false to avoid PostgreSQL deadlocks when creating members and groups
in the same test run (same as IndexGroupsDisplayTest). 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 use MvWeb.ConnCase, async: false
import Phoenix.LiveViewTest import Phoenix.LiveViewTest
@ -97,10 +93,11 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do
member: member member: member
} do } do
conn = conn_with_oidc_user(conn) 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 # Groups are in Personal Data; label "Groups" and empty state "No groups" must be present
assert has_element?(view, "main section h2", gettext("Groups")) assert html =~ gettext("Groups")
assert html =~ gettext("No groups")
end end
test "groups are loaded with member (single request returns all group names)", %{ test "groups are loaded with member (single request returns all group names)", %{