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)", %{