From cbadaa8b14dff4299178cb77251150fef786fc10 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 17 Feb 2026 00:16:28 +0000 Subject: [PATCH 01/10] chore(deps): update renovate/renovate docker tag to v43 --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 2c8d504..ed66e3e 100644 --- a/.drone.yml +++ b/.drone.yml @@ -273,7 +273,7 @@ environment: steps: - name: renovate - image: renovate/renovate:42.97 + image: renovate/renovate:43.19 environment: RENOVATE_CONFIG_FILE: "renovate_backend_config.js" RENOVATE_TOKEN: From 2e4d14dd607d5ae6f3ba6c42ead58b03b5999c89 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 17 Feb 2026 12:15:46 +0100 Subject: [PATCH 02/10] test: add tdd tests for groups in member detail view #374 --- .../member_live/show_groups_display_test.exs | 323 ++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 test/mv_web/member_live/show_groups_display_test.exs diff --git a/test/mv_web/member_live/show_groups_display_test.exs b/test/mv_web/member_live/show_groups_display_test.exs new file mode 100644 index 0000000..49d5796 --- /dev/null +++ b/test/mv_web/member_live/show_groups_display_test.exs @@ -0,0 +1,323 @@ +defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do + @moduledoc """ + 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 + - Edge cases (one group, many groups) + - Security: groups section visible only when user may view member + - Accessibility: badges 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 + require Ash.Query + use Gettext, backend: MvWeb.Gettext + + alias Mv.Membership.{Group, MemberGroup} + + describe "groups section" do + setup do + system_actor = Mv.Helpers.SystemActor.get_system_actor() + + {:ok, member} = + Mv.Membership.create_member( + %{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"}, + actor: system_actor + ) + + %{member: member, actor: system_actor} + end + + test "displays Groups section when member has at least one group", %{ + conn: conn, + member: member, + actor: actor + } do + {:ok, group} = + Group + |> Ash.Changeset.for_create(:create, %{name: "Board Members"}) + |> Ash.create(actor: actor) + + {:ok, _mg} = + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) + |> Ash.create(actor: actor) + + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, ~p"/members/#{member}") + + assert html =~ gettext("Groups") + assert html =~ group.name + end + + test "displays all groups as badges with correct names when member is in multiple groups", %{ + conn: conn, + member: member, + actor: actor + } do + {:ok, group1} = + Group + |> Ash.Changeset.for_create(:create, %{name: "Board Members"}) + |> Ash.create(actor: actor) + + {:ok, group2} = + Group + |> Ash.Changeset.for_create(:create, %{name: "Active Members"}) + |> Ash.create(actor: actor) + + {:ok, _mg1} = + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group1.id}) + |> Ash.create(actor: actor) + + {:ok, _mg2} = + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group2.id}) + |> Ash.create(actor: actor) + + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, ~p"/members/#{member}") + + assert html =~ gettext("Groups") + assert html =~ group1.name + assert html =~ group2.name + end + + test "displays Groups section when member has no groups (empty state)", %{ + conn: conn, + member: member + } do + conn = conn_with_oidc_user(conn) + {: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")) + end + + test "groups are loaded with member (single request returns all group names)", %{ + conn: conn, + member: member, + actor: actor + } do + {:ok, group1} = + Group + |> Ash.Changeset.for_create(:create, %{name: "Alpha"}) + |> Ash.create(actor: actor) + + {:ok, group2} = + Group + |> Ash.Changeset.for_create(:create, %{name: "Beta"}) + |> Ash.create(actor: actor) + + {:ok, _mg1} = + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group1.id}) + |> Ash.create(actor: actor) + + {:ok, _mg2} = + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group2.id}) + |> Ash.create(actor: actor) + + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, ~p"/members/#{member}") + + assert html =~ "Alpha" + assert html =~ "Beta" + end + end + + describe "groups section links" do + setup do + system_actor = Mv.Helpers.SystemActor.get_system_actor() + + {:ok, member} = + Mv.Membership.create_member( + %{first_name: "Bob", last_name: "Brown", email: "bob@example.com"}, + actor: system_actor + ) + + {:ok, group} = + Group + |> Ash.Changeset.for_create(:create, %{name: "Board Members"}) + |> Ash.create(actor: system_actor) + + {:ok, _mg} = + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) + |> Ash.create(actor: system_actor) + + %{member: member, group: group} + end + + test "each group badge links to group detail page with correct slug", %{ + conn: conn, + member: member, + group: group + } do + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, ~p"/members/#{member}") + + # Link to group detail: /groups/:slug (slug is URL-friendly, e.g. "board-members") + assert html =~ ~r/href="[^"]*\/groups\/#{Regex.escape(group.slug)}"|navigate="[^"]*\/groups\/#{Regex.escape(group.slug)}"/ + end + + test "clicking group badge navigates to group detail page", %{ + conn: conn, + member: member, + group: group + } do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, ~p"/members/#{member}") + + view + |> element("a[href*='/groups/#{group.slug}']") + |> render_click() + + assert_redirect(view, ~p"/groups/#{group.slug}") + end + end + + describe "groups section edge cases" do + setup do + system_actor = Mv.Helpers.SystemActor.get_system_actor() + + {:ok, member} = + Mv.Membership.create_member( + %{first_name: "Charlie", last_name: "Clark", email: "charlie@example.com"}, + actor: system_actor + ) + + %{member: member, actor: system_actor} + end + + test "member in exactly one group shows single badge", %{ + conn: conn, + member: member, + actor: actor + } do + {:ok, group} = + Group + |> Ash.Changeset.for_create(:create, %{name: "Solo Group"}) + |> Ash.create(actor: actor) + + {:ok, _mg} = + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) + |> Ash.create(actor: actor) + + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, ~p"/members/#{member}") + + assert html =~ gettext("Groups") + assert html =~ group.name + end + + test "member in many groups shows all badges", %{ + conn: conn, + member: member, + actor: actor + } do + group_names = Enum.map(1..5, fn i -> "Group #{i}" end) + + groups = + Enum.map(group_names, fn name -> + {:ok, g} = + Group + |> Ash.Changeset.for_create(:create, %{name: name}) + |> Ash.create(actor: actor) + + g + end) + + for g <- groups do + {:ok, _mg} = + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: g.id}) + |> Ash.create(actor: actor) + end + + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, ~p"/members/#{member}") + + assert html =~ gettext("Groups") + for name <- group_names do + assert html =~ name + end + end + end + + describe "groups section with read_only user" do + @tag role: :read_only + test "user with read permission sees Groups section", %{conn: conn} do + system_actor = Mv.Helpers.SystemActor.get_system_actor() + + {:ok, member} = + Mv.Membership.create_member( + %{first_name: "Diana", last_name: "Davis", email: "diana@example.com"}, + actor: system_actor + ) + + {:ok, group} = + Group + |> Ash.Changeset.for_create(:create, %{name: "Readers"}) + |> Ash.create(actor: system_actor) + + {:ok, _mg} = + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) + |> Ash.create(actor: system_actor) + + {:ok, _view, html} = live(conn, ~p"/members/#{member}") + + assert html =~ gettext("Groups") + assert html =~ group.name + end + end + + describe "groups section accessibility" do + setup do + system_actor = Mv.Helpers.SystemActor.get_system_actor() + + {:ok, member} = + Mv.Membership.create_member( + %{first_name: "Eve", last_name: "Evans", email: "eve@example.com"}, + actor: system_actor + ) + + {:ok, group} = + Group + |> Ash.Changeset.for_create(:create, %{name: "A11y Group"}) + |> Ash.create(actor: system_actor) + + {:ok, _mg} = + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) + |> Ash.create(actor: system_actor) + + %{member: member, group: group} + end + + test "group badges have role and aria-label for screen readers", %{ + conn: conn, + member: member, + group: group + } do + conn = conn_with_oidc_user(conn) + {:ok, view, html} = live(conn, ~p"/members/#{member}") + + assert html =~ group.name + # Badge has role="status" and aria-label indicating group membership (architecture: "Member of group X") + assert has_element?(view, "[role='status'][aria-label*='#{group.name}']") + end + end +end From 46f9094e1fabd78293229c515ed74df4023ef02b Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 17 Feb 2026 12:16:15 +0100 Subject: [PATCH 03/10] style: fix formatting --- test/mv_web/member_live/show_groups_display_test.exs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 49d5796..78112ae 100644 --- a/test/mv_web/member_live/show_groups_display_test.exs +++ b/test/mv_web/member_live/show_groups_display_test.exs @@ -168,7 +168,8 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do {:ok, _view, html} = live(conn, ~p"/members/#{member}") # Link to group detail: /groups/:slug (slug is URL-friendly, e.g. "board-members") - assert html =~ ~r/href="[^"]*\/groups\/#{Regex.escape(group.slug)}"|navigate="[^"]*\/groups\/#{Regex.escape(group.slug)}"/ + assert html =~ + ~r/href="[^"]*\/groups\/#{Regex.escape(group.slug)}"|navigate="[^"]*\/groups\/#{Regex.escape(group.slug)}"/ end test "clicking group badge navigates to group detail page", %{ @@ -250,6 +251,7 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do {:ok, _view, html} = live(conn, ~p"/members/#{member}") assert html =~ gettext("Groups") + for name <- group_names do assert html =~ name end @@ -316,6 +318,7 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do {:ok, view, html} = live(conn, ~p"/members/#{member}") assert html =~ group.name + # Badge has role="status" and aria-label indicating group membership (architecture: "Member of group X") assert has_element?(view, "[role='status'][aria-label*='#{group.name}']") end From b1a9eb8b1d8fb1c3f460735eb4fb8e8d21590247 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 17 Feb 2026 14:15:43 +0100 Subject: [PATCH 04/10] feat: add groups to member detail view #374 --- docs/groups-architecture.md | 2 ++ lib/mv_web/live/member_live/show.ex | 27 ++++++++++++++++++- priv/gettext/de/LC_MESSAGES/default.po | 3 +++ priv/gettext/default.pot | 3 +++ priv/gettext/en/LC_MESSAGES/default.po | 3 +++ .../member_live/show_groups_display_test.exs | 19 ++++++------- 6 files changed, 45 insertions(+), 12 deletions(-) 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)", %{ From 911f308a67c93b49dc9a3444542744b91e49fe99 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 17 Feb 2026 15:30:23 +0100 Subject: [PATCH 05/10] fix: address review comments --- docs/groups-architecture.md | 6 +- lib/mv_web/live/member_live/index.html.heex | 1 - lib/mv_web/live/member_live/show.ex | 14 +- priv/gettext/de/LC_MESSAGES/default.po | 9 - priv/gettext/en/LC_MESSAGES/default.po | 5 - .../index_groups_accessibility_test.exs | 8 +- .../member_live/show_groups_display_test.exs | 209 +++++++----------- 7 files changed, 88 insertions(+), 164 deletions(-) diff --git a/docs/groups-architecture.md b/docs/groups-architecture.md index 1557b6f..0e59409 100644 --- a/docs/groups-architecture.md +++ b/docs/groups-architecture.md @@ -356,9 +356,9 @@ lib/ - Screen readers must be able to navigate and understand the interface - ARIA labels and roles must be properly set -**Group Badges in Member Overview:** -- Badges must have `role="status"` and appropriate `aria-label` attributes -- Badge title should indicate group membership +**Group Badges and Links in Member Overview / Detail:** +- Use `aria-label` to indicate group membership (e.g. "Member of group X"). Do not use `role="status"` on badges or links—that role is for live regions (screen reader announcements), not for navigation or static labels. +- Badge/link text or title should indicate group membership for screen readers. **Clickable Group Badge (for filtering) - Optional:** diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index 311447b..b490618 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -322,7 +322,6 @@ <%= for group <- (member.groups || []) do %> {group.name} diff --git a/lib/mv_web/live/member_live/show.ex b/lib/mv_web/live/member_live/show.ex index cc0b25e..47e8878 100644 --- a/lib/mv_web/live/member_live/show.ex +++ b/lib/mv_web/live/member_live/show.ex @@ -12,7 +12,7 @@ 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 + - Groups: Links to group detail pages in Personal Data section - Payment Data: Membership fee type and cycle status - Membership Fees: Tab showing all membership fee cycles with status management (via MembershipFeesComponent) @@ -148,24 +148,24 @@ defmodule MvWeb.MemberLive.Show do
<% end %> - <%!-- Groups (in Personal Data, below Linked User) --%> + <%!-- Groups (in Personal Data) --%> + <% groups = @member.groups || [] %>
<.data_field label={gettext("Groups")}> - <%= if Enum.any?(@member.groups || []) do %> + <%= if Enum.empty?(groups) do %> + {gettext("No groups")} + <% else %>
- <%= for group <- (@member.groups || []) do %> + <%= for group <- 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 %>
diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 57334d2..e40d053 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -2616,12 +2616,3 @@ msgstr "Anzahl Mitglieder:" #, elixir-autogen, elixir-format msgid "PDF" msgstr "PDF" - -#~ #: lib/mv_web/live/global_settings_live.ex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Custom Fields in CSV Import" -#~ msgstr "Benutzerdefinierte Felder" - -#~ #, elixir-autogen, elixir-format -#~ msgid "Failed to prepare CSV import: %{error}" -#~ msgstr "Das Vorbereiten des CSV Imports ist gescheitert: %{error}" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 1bf276c..f12d478 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -2617,8 +2617,3 @@ msgstr "Member count:" #, elixir-autogen, elixir-format msgid "PDF" msgstr "" - -#~ #: lib/mv_web/live/global_settings_live.ex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Custom Fields in CSV Import" -#~ msgstr "" diff --git a/test/mv_web/member_live/index_groups_accessibility_test.exs b/test/mv_web/member_live/index_groups_accessibility_test.exs index ab9b728..d14cd9f 100644 --- a/test/mv_web/member_live/index_groups_accessibility_test.exs +++ b/test/mv_web/member_live/index_groups_accessibility_test.exs @@ -3,7 +3,7 @@ defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do Tests for accessibility of groups feature in the member overview. Tests cover: - - Badges have role="status" and aria-label + - Badges have aria-label for group membership (no role="status"; reserved for live regions) - Filter dropdown has aria-label - Sort header has aria-label for screen reader - Keyboard navigation works (Tab through filter, sort header) @@ -44,7 +44,7 @@ defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do end @tag :ui - test "group badges have role and aria-label", %{ + test "group badges have aria-label for screen readers", %{ conn: conn, member1: member1, group1: group1 @@ -52,8 +52,8 @@ defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do conn = conn_with_oidc_user(conn) {:ok, view, html} = live(conn, "/members") - # Verify badges have role="status" and aria-label containing the group name - assert has_element?(view, "span[role='status'][aria-label*='#{group1.name}']") + # Verify badges have aria-label containing the group name (no role=status on badges) + assert has_element?(view, "span[aria-label*='#{group1.name}']") assert html =~ group1.name # Verify member1's row contains the badge 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 bda46b3..f8434b3 100644 --- a/test/mv_web/member_live/show_groups_display_test.exs +++ b/test/mv_web/member_live/show_groups_display_test.exs @@ -4,10 +4,10 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do Tests cover: - Groups in Personal Data (with and without groups) - - Group buttons with correct names and links to group detail pages + - Group links with correct names and links to group detail pages - Edge cases (one group, many groups) - Security: groups visible only when user may view member - - Accessibility: group links have role and aria-label + - Accessibility: group links have aria-label for screen readers ## Note on async async: false to avoid PostgreSQL deadlocks when creating members and groups @@ -22,15 +22,16 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do describe "groups section" do setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() + actor = Mv.Helpers.SystemActor.get_system_actor() {:ok, member} = - Mv.Membership.create_member( - %{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"}, - actor: system_actor - ) + create_member(actor, %{ + first_name: "Alice", + last_name: "Anderson", + email: "alice@example.com" + }) - %{member: member, actor: system_actor} + %{member: member, actor: actor} end test "displays Groups section when member has at least one group", %{ @@ -38,15 +39,8 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do member: member, actor: actor } do - {:ok, group} = - Group - |> Ash.Changeset.for_create(:create, %{name: "Board Members"}) - |> Ash.create(actor: actor) - - {:ok, _mg} = - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) - |> Ash.create(actor: actor) + {:ok, group} = create_group(actor, "Board Members") + {:ok, _mg} = add_member_to_group(member, group, actor) conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, ~p"/members/#{member}") @@ -55,30 +49,15 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do assert html =~ group.name end - test "displays all groups as badges with correct names when member is in multiple groups", %{ + test "displays all group links with correct names when member is in multiple groups", %{ conn: conn, member: member, actor: actor } do - {:ok, group1} = - Group - |> Ash.Changeset.for_create(:create, %{name: "Board Members"}) - |> Ash.create(actor: actor) - - {:ok, group2} = - Group - |> Ash.Changeset.for_create(:create, %{name: "Active Members"}) - |> Ash.create(actor: actor) - - {:ok, _mg1} = - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group1.id}) - |> Ash.create(actor: actor) - - {:ok, _mg2} = - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group2.id}) - |> Ash.create(actor: actor) + {:ok, group1} = create_group(actor, "Board Members") + {:ok, group2} = create_group(actor, "Active Members") + {:ok, _mg1} = add_member_to_group(member, group1, actor) + {:ok, _mg2} = add_member_to_group(member, group2, actor) conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, ~p"/members/#{member}") @@ -100,30 +79,15 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do assert html =~ gettext("No groups") end - test "groups are loaded with member (single request returns all group names)", %{ + test "renders all group names when member has multiple groups", %{ conn: conn, member: member, actor: actor } do - {:ok, group1} = - Group - |> Ash.Changeset.for_create(:create, %{name: "Alpha"}) - |> Ash.create(actor: actor) - - {:ok, group2} = - Group - |> Ash.Changeset.for_create(:create, %{name: "Beta"}) - |> Ash.create(actor: actor) - - {:ok, _mg1} = - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group1.id}) - |> Ash.create(actor: actor) - - {:ok, _mg2} = - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group2.id}) - |> Ash.create(actor: actor) + {:ok, group1} = create_group(actor, "Alpha") + {:ok, group2} = create_group(actor, "Beta") + {:ok, _mg1} = add_member_to_group(member, group1, actor) + {:ok, _mg2} = add_member_to_group(member, group2, actor) conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, ~p"/members/#{member}") @@ -135,41 +99,29 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do describe "groups section links" do setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() + actor = Mv.Helpers.SystemActor.get_system_actor() {:ok, member} = - Mv.Membership.create_member( - %{first_name: "Bob", last_name: "Brown", email: "bob@example.com"}, - actor: system_actor - ) - - {:ok, group} = - Group - |> Ash.Changeset.for_create(:create, %{name: "Board Members"}) - |> Ash.create(actor: system_actor) - - {:ok, _mg} = - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) - |> Ash.create(actor: system_actor) + create_member(actor, %{first_name: "Bob", last_name: "Brown", email: "bob@example.com"}) + {:ok, group} = create_group(actor, "Board Members") + {:ok, _mg} = add_member_to_group(member, group, actor) %{member: member, group: group} end - test "each group badge links to group detail page with correct slug", %{ + test "each group link goes to group detail page with correct slug", %{ conn: conn, member: member, group: group } do conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, ~p"/members/#{member}") + {:ok, view, _html} = live(conn, ~p"/members/#{member}") # Link to group detail: /groups/:slug (slug is URL-friendly, e.g. "board-members") - assert html =~ - ~r/href="[^"]*\/groups\/#{Regex.escape(group.slug)}"|navigate="[^"]*\/groups\/#{Regex.escape(group.slug)}"/ + assert has_element?(view, "a[href*='/groups/#{group.slug}']", group.name) end - test "clicking group badge navigates to group detail page", %{ + test "clicking group link navigates to group detail page", %{ conn: conn, member: member, group: group @@ -187,31 +139,25 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do describe "groups section edge cases" do setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() + actor = Mv.Helpers.SystemActor.get_system_actor() {:ok, member} = - Mv.Membership.create_member( - %{first_name: "Charlie", last_name: "Clark", email: "charlie@example.com"}, - actor: system_actor - ) + create_member(actor, %{ + first_name: "Charlie", + last_name: "Clark", + email: "charlie@example.com" + }) - %{member: member, actor: system_actor} + %{member: member, actor: actor} end - test "member in exactly one group shows single badge", %{ + test "member in exactly one group shows single link", %{ conn: conn, member: member, actor: actor } do - {:ok, group} = - Group - |> Ash.Changeset.for_create(:create, %{name: "Solo Group"}) - |> Ash.create(actor: actor) - - {:ok, _mg} = - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) - |> Ash.create(actor: actor) + {:ok, group} = create_group(actor, "Solo Group") + {:ok, _mg} = add_member_to_group(member, group, actor) conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, ~p"/members/#{member}") @@ -220,7 +166,7 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do assert html =~ group.name end - test "member in many groups shows all badges", %{ + test "member in many groups shows all group links", %{ conn: conn, member: member, actor: actor @@ -229,19 +175,12 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do groups = Enum.map(group_names, fn name -> - {:ok, g} = - Group - |> Ash.Changeset.for_create(:create, %{name: name}) - |> Ash.create(actor: actor) - + {:ok, g} = create_group(actor, name) g end) for g <- groups do - {:ok, _mg} = - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: g.id}) - |> Ash.create(actor: actor) + {:ok, _mg} = add_member_to_group(member, g, actor) end conn = conn_with_oidc_user(conn) @@ -258,23 +197,17 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do describe "groups section with read_only user" do @tag role: :read_only test "user with read permission sees Groups section", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() + actor = Mv.Helpers.SystemActor.get_system_actor() {:ok, member} = - Mv.Membership.create_member( - %{first_name: "Diana", last_name: "Davis", email: "diana@example.com"}, - actor: system_actor - ) + create_member(actor, %{ + first_name: "Diana", + last_name: "Davis", + email: "diana@example.com" + }) - {:ok, group} = - Group - |> Ash.Changeset.for_create(:create, %{name: "Readers"}) - |> Ash.create(actor: system_actor) - - {:ok, _mg} = - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) - |> Ash.create(actor: system_actor) + {:ok, group} = create_group(actor, "Readers") + {:ok, _mg} = add_member_to_group(member, group, actor) {:ok, _view, html} = live(conn, ~p"/members/#{member}") @@ -285,28 +218,17 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do describe "groups section accessibility" do setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() + actor = Mv.Helpers.SystemActor.get_system_actor() {:ok, member} = - Mv.Membership.create_member( - %{first_name: "Eve", last_name: "Evans", email: "eve@example.com"}, - actor: system_actor - ) - - {:ok, group} = - Group - |> Ash.Changeset.for_create(:create, %{name: "A11y Group"}) - |> Ash.create(actor: system_actor) - - {:ok, _mg} = - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) - |> Ash.create(actor: system_actor) + create_member(actor, %{first_name: "Eve", last_name: "Evans", email: "eve@example.com"}) + {:ok, group} = create_group(actor, "A11y Group") + {:ok, _mg} = add_member_to_group(member, group, actor) %{member: member, group: group} end - test "group badges have role and aria-label for screen readers", %{ + test "group links have aria-label for screen readers", %{ conn: conn, member: member, group: group @@ -316,8 +238,25 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do assert html =~ group.name - # Badge has role="status" and aria-label indicating group membership (architecture: "Member of group X") - assert has_element?(view, "[role='status'][aria-label*='#{group.name}']") + # Group link has aria-label indicating group membership for screen readers + assert has_element?(view, "a[aria-label*='#{group.name}']") end end + + # Helpers to reduce setup duplication (create member/group, assign member to group). + defp create_member(actor, attrs) do + Mv.Membership.create_member(attrs, actor: actor) + end + + defp create_group(actor, name) do + Group + |> Ash.Changeset.for_create(:create, %{name: name}) + |> Ash.create(actor: actor) + end + + defp add_member_to_group(member, group, actor) do + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) + |> Ash.create(actor: actor) + end end From cecb547bd642a663d53fd17fcb412886fb33165f Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 16 Feb 2026 16:53:37 +0100 Subject: [PATCH 06/10] bug: adds membership startdate column --- lib/mv_web/live/member_live/index.html.heex | 107 ++++++++++++-------- 1 file changed, 66 insertions(+), 41 deletions(-) diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index b490618..f8be88d 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -180,6 +180,67 @@ > {member.email} + <:col + :let={member} + :if={:join_date in @member_fields_visible} + 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} + /> + """ + } + > + {MvWeb.MemberLive.Index.format_date(member.join_date)} + + <:col + :let={member} + :if={:exit_date in @member_fields_visible} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_exit_date} + field={:exit_date} + label={gettext("Exit Date")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + {MvWeb.MemberLive.Index.format_date(member.exit_date)} + + <:col + :let={member} + :if={:notes in @member_fields_visible} + label={gettext("Notes")} + > + {member.notes} + + <:col + :let={member} + :if={:city in @member_fields_visible} + 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} :if={:street in @member_fields_visible} @@ -236,57 +297,21 @@ <:col :let={member} - :if={:city in @member_fields_visible} + :if={:membership_fee_start_date in @member_fields_visible} label={ ~H""" <.live_component module={MvWeb.Components.SortHeaderComponent} - id={:sort_city} - field={:city} - label={gettext("City")} + id={:sort_membership_fee_start_date} + field={:membership_fee_start_date} + label={gettext("Membership Fee Start Date")} sort_field={@sort_field} sort_order={@sort_order} /> """ } > - {member.city} - - <:col - :let={member} - :if={:join_date in @member_fields_visible} - 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} - /> - """ - } - > - {MvWeb.MemberLive.Index.format_date(member.join_date)} - - <:col - :let={member} - :if={:exit_date in @member_fields_visible} - label={ - ~H""" - <.live_component - module={MvWeb.Components.SortHeaderComponent} - id={:sort_exit_date} - field={:exit_date} - label={gettext("Exit Date")} - sort_field={@sort_field} - sort_order={@sort_order} - /> - """ - } - > - {MvWeb.MemberLive.Index.format_date(member.exit_date)} + {MvWeb.MemberLive.Index.format_date(member.membership_fee_start_date)} <:col :let={member} From 49bd2eee0b0663dbd7c8592429681f3eb11c3019 Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 17 Feb 2026 17:59:30 +0100 Subject: [PATCH 07/10] i18n: update translations --- priv/gettext/de/LC_MESSAGES/default.po | 2 ++ priv/gettext/default.pot | 2 ++ priv/gettext/en/LC_MESSAGES/default.po | 2 ++ 3 files changed, 6 insertions(+) diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index e40d053..0187da6 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -144,6 +144,7 @@ msgid "House Number" msgstr "Hausnummer" #: lib/mv_web/live/member_live/form.ex +#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/show.ex #: lib/mv_web/translations/member_fields.ex #, elixir-autogen, elixir-format @@ -1542,6 +1543,7 @@ msgstr "Du bist dabei alle %{count} Zyklen für dieses Mitglied zu löschen." msgid "Delete Membership Fee Type" msgstr "Mitgliedsbeitragsart löschen" +#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/translations/member_fields.ex #, elixir-autogen, elixir-format, fuzzy msgid "Membership Fee Start Date" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index fb156df..a05597c 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -145,6 +145,7 @@ msgid "House Number" msgstr "" #: lib/mv_web/live/member_live/form.ex +#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/show.ex #: lib/mv_web/translations/member_fields.ex #, elixir-autogen, elixir-format @@ -1543,6 +1544,7 @@ msgstr "" msgid "Delete Membership Fee Type" msgstr "" +#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/translations/member_fields.ex #, elixir-autogen, elixir-format msgid "Membership Fee Start Date" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index f12d478..261cbe4 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -145,6 +145,7 @@ msgid "House Number" msgstr "" #: lib/mv_web/live/member_live/form.ex +#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/show.ex #: lib/mv_web/translations/member_fields.ex #, elixir-autogen, elixir-format @@ -1543,6 +1544,7 @@ msgstr "" msgid "Delete Membership Fee Type" msgstr "" +#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/translations/member_fields.ex #, elixir-autogen, elixir-format, fuzzy msgid "Membership Fee Start Date" From ce542eae3eacd51398a312bab339e22ef5879025 Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 17 Feb 2026 18:26:32 +0100 Subject: [PATCH 08/10] fix: missing actor on tturning back from edit --- lib/mv_web/live/custom_field_live/index_component.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/mv_web/live/custom_field_live/index_component.ex b/lib/mv_web/live/custom_field_live/index_component.ex index 5cf0f6b..a670a3e 100644 --- a/lib/mv_web/live/custom_field_live/index_component.ex +++ b/lib/mv_web/live/custom_field_live/index_component.ex @@ -219,6 +219,9 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do send(self(), {:editing_section_changed, nil}) end + # Get actor from assigns or fall back to socket assigns + actor = Map.get(assigns, :actor, socket.assigns[:actor]) + {:ok, socket |> assign(assigns) @@ -228,7 +231,7 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do |> assign_new(:show_delete_modal, fn -> false end) |> assign_new(:custom_field_to_delete, fn -> nil end) |> assign_new(:slug_confirmation, fn -> "" end) - |> stream(:custom_fields, stream_custom_fields(assigns[:actor], self()), reset: true)} + |> stream(:custom_fields, stream_custom_fields(actor, self()), reset: true)} end @impl true From b18f895939e1cc68718b2842b19b41110ae2946a Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 17 Feb 2026 18:56:56 +0100 Subject: [PATCH 09/10] chore: rename ImportExport module to Import --- lib/mv/membership/import/import_runner.ex | 2 +- lib/mv_web/components/layouts/sidebar.ex | 2 +- .../{import_export_live.ex => import_live.ex} | 27 +---- .../components.ex | 4 +- lib/mv_web/router.ex | 4 +- priv/gettext/de/LC_MESSAGES/default.po | 98 +++++++++---------- priv/gettext/default.pot | 83 +++++++--------- priv/gettext/en/LC_MESSAGES/default.po | 98 +++++++++---------- .../live/global_settings_live_config_test.exs | 2 +- ...ort_live_test.exs => import_live_test.exs} | 38 ++++--- 10 files changed, 161 insertions(+), 197 deletions(-) rename lib/mv_web/live/{import_export_live.ex => import_live.ex} (94%) rename lib/mv_web/live/{import_export_live => import_live}/components.ex (98%) rename test/mv_web/live/{import_export_live_test.exs => import_live_test.exs} (87%) diff --git a/lib/mv/membership/import/import_runner.ex b/lib/mv/membership/import/import_runner.ex index 5ff846b..eccd75f 100644 --- a/lib/mv/membership/import/import_runner.ex +++ b/lib/mv/membership/import/import_runner.ex @@ -1,7 +1,7 @@ defmodule Mv.Membership.Import.ImportRunner do @moduledoc """ Orchestrates CSV member import: file reading, progress tracking, chunk processing, - and error formatting. Used by `MvWeb.ImportExportLive` to keep LiveView thin. + and error formatting. Used by `MvWeb.ImportLive` to keep LiveView thin. This module does not depend on Phoenix or LiveView. It provides pure functions for progress/merge and side-effectful helpers (read_file_entry, process_chunk) that diff --git a/lib/mv_web/components/layouts/sidebar.ex b/lib/mv_web/components/layouts/sidebar.ex index 1896f24..8ed7f03 100644 --- a/lib/mv_web/components/layouts/sidebar.ex +++ b/lib/mv_web/components/layouts/sidebar.ex @@ -118,7 +118,7 @@ defmodule MvWeb.Layouts.Sidebar do /> <% end %> <%= if can_access_page?(@current_user, PagePaths.settings()) do %> - <.menu_subitem href={~p"/admin/import-export"} label={gettext("Import/Export")} /> + <.menu_subitem href={~p"/admin/import"} label={gettext("Import")} /> <.menu_subitem href={~p"/settings"} label={gettext("Settings")} /> <% end %> diff --git a/lib/mv_web/live/import_export_live.ex b/lib/mv_web/live/import_live.ex similarity index 94% rename from lib/mv_web/live/import_export_live.ex rename to lib/mv_web/live/import_live.ex index fe5484c..e97ecd7 100644 --- a/lib/mv_web/live/import_export_live.ex +++ b/lib/mv_web/live/import_live.ex @@ -1,6 +1,6 @@ -defmodule MvWeb.ImportExportLive do +defmodule MvWeb.ImportLive do @moduledoc """ - LiveView for importing and exporting members via CSV. + LiveView for importing members via CSV. ## Features - CSV member import (admin only) @@ -38,7 +38,7 @@ defmodule MvWeb.ImportExportLive do alias Mv.Membership.Import.ImportRunner alias Mv.Membership.Import.MemberCSV alias MvWeb.Authorization - alias MvWeb.ImportExportLive.Components + alias MvWeb.ImportLive.Components on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded} @@ -65,7 +65,7 @@ defmodule MvWeb.ImportExportLive do socket = socket - |> assign(:page_title, gettext("Import/Export")) + |> assign(:page_title, gettext("Import")) |> assign(:club_name, club_name) |> assign(:import_state, nil) |> assign(:import_progress, nil) @@ -90,13 +90,6 @@ defmodule MvWeb.ImportExportLive do def render(assigns) do ~H""" - <.header> - {gettext("Import/Export")} - <:subtitle> - {gettext("Import members from CSV files or export member data.")} - - - <%= if Authorization.can?(@current_user, :create, Mv.Membership.Member) do %> <%!-- CSV Import Section --%> <.form_section title={gettext("Import Members (CSV)")}> @@ -107,18 +100,6 @@ defmodule MvWeb.ImportExportLive do <% end %> - - <%!-- Export Section (Placeholder) --%> - <.form_section title={gettext("Export Members (CSV)")}> -
- <.icon name="hero-information-circle" class="size-5" aria-hidden="true" /> -
-

- {gettext("Export functionality will be available in a future release.")} -

-
-
- <% else %>