Compare commits

..

12 commits

Author SHA1 Message Date
Renovate Bot
225bf8b681 chore(deps): update renovate/renovate docker tag to v43
Some checks reported errors
continuous-integration/drone/push Build was killed
2026-02-18 00:20:15 +00:00
e99dbdfb82 Merge pull request 'Fixes empty custom fields while turning back in settings closes #413' (#425) from bug/413_turn_back_custom into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #425
2026-02-17 19:30:11 +01:00
b18f895939 chore: rename ImportExport module to Import
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2026-02-17 18:59:18 +01:00
ce542eae3e fix: missing actor on tturning back from edit 2026-02-17 18:59:18 +01:00
2b1f49d60a Merge pull request 'Implements missing member columns closes #416 and #419' (#424) from bug/416_member_columns into main
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #424
2026-02-17 18:17:16 +01:00
49bd2eee0b i18n: update translations
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2026-02-17 17:59:30 +01:00
cecb547bd6 bug: adds membership startdate column
Some checks failed
continuous-integration/drone/push Build is failing
2026-02-17 17:54:25 +01:00
3f07de1276 Merge pull request 'Add groups to member detail view closes #374' (#423) from feature/374-member-detail-groups into main
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #423
2026-02-17 15:50:44 +01:00
911f308a67
fix: address review comments
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/promote/production Build is passing
2026-02-17 15:30:23 +01:00
b1a9eb8b1d
feat: add groups to member detail view #374
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-17 14:15:43 +01:00
46f9094e1f
style: fix formatting
Some checks failed
continuous-integration/drone/push Build is failing
2026-02-17 12:16:15 +01:00
2e4d14dd60
test: add tdd tests for groups in member detail view #374
Some checks failed
continuous-integration/drone/push Build is failing
2026-02-17 12:15:46 +01:00
17 changed files with 538 additions and 257 deletions

View file

@ -273,7 +273,7 @@ environment:
steps:
- name: renovate
image: renovate/renovate:43.19
image: renovate/renovate:43.24
environment:
RENOVATE_CONFIG_FILE: "renovate_backend_config.js"
RENOVATE_TOKEN:

View file

@ -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:**
@ -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

View file

@ -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

View file

@ -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 %>
</.menu_group>

View file

@ -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

View file

@ -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"""
<Layouts.app flash={@flash} current_user={@current_user} club_name={@club_name}>
<.header>
{gettext("Import/Export")}
<:subtitle>
{gettext("Import members from CSV files or export member data.")}
</:subtitle>
</.header>
<%= 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
<Components.import_progress {assigns} />
<% end %>
</.form_section>
<%!-- Export Section (Placeholder) --%>
<.form_section title={gettext("Export Members (CSV)")}>
<div role="note" class="alert alert-info">
<.icon name="hero-information-circle" class="size-5" aria-hidden="true" />
<div>
<p class="text-sm">
{gettext("Export functionality will be available in a future release.")}
</p>
</div>
</div>
</.form_section>
<% else %>
<div role="alert" class="alert alert-error">
<.icon name="hero-exclamation-circle" class="size-5" aria-hidden="true" />

View file

@ -1,6 +1,6 @@
defmodule MvWeb.ImportExportLive.Components do
defmodule MvWeb.ImportLive.Components do
@moduledoc """
Function components for the Import/Export LiveView: import form, progress, results,
Function components for the Import LiveView: import form, progress, results,
custom fields notice, and template links. Keeps the main LiveView focused on
mount/handle_event/handle_info and glue code.
"""

View file

@ -180,6 +180,67 @@
>
{member.email}
</:col>
<: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>
<: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>
<:col
:let={member}
:if={:notes in @member_fields_visible}
label={gettext("Notes")}
>
{member.notes}
</:col>
<: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>
<:col
:let={member}
:if={:street in @member_fields_visible}
@ -236,57 +297,21 @@
</:col>
<: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>
<: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>
<: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>
<:col
:let={member}
@ -322,7 +347,6 @@
<%= for group <- (member.groups || []) do %>
<span
class="badge badge-outline badge-primary"
role="status"
aria-label={gettext("Member of group %{name}", name: group.name)}
>
{group.name}

View file

@ -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: 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)
## Navigation
@ -146,6 +148,28 @@ defmodule MvWeb.MemberLive.Show do
</div>
<% end %>
<%!-- Groups (in Personal Data) --%>
<% groups = @member.groups || [] %>
<div>
<.data_field label={gettext("Groups")}>
<%= if Enum.empty?(groups) do %>
<span class="text-base-content/70 italic">{gettext("No groups")}</span>
<% else %>
<div class="flex flex-wrap gap-2">
<%= for group <- groups do %>
<.link
navigate={~p"/groups/#{group.slug}"}
class="btn btn-xs btn-outline btn-primary"
aria-label={gettext("Member of group %{name}", name: group.name)}
>
{group.name}
</.link>
<% end %>
</div>
<% end %>
</.data_field>
</div>
<%!-- Notes --%>
<%= if @member.notes && String.trim(@member.notes) != "" do %>
<div>
@ -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)

View file

@ -91,8 +91,8 @@ defmodule MvWeb.Router do
live "/admin/roles/:id", RoleLive.Show, :show
live "/admin/roles/:id/edit", RoleLive.Form, :edit
# Import/Export (Admin only)
live "/admin/import-export", ImportExportLive
# Import (Admin only)
live "/admin/import", ImportLive
post "/members/export.csv", MemberExportController, :export
post "/members/export.pdf", MemberPdfExportController, :export

View file

@ -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"
@ -1859,7 +1861,7 @@ msgstr "erstellt"
msgid "updated"
msgstr "aktualisiert"
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format
msgid "Unknown error"
@ -1980,37 +1982,37 @@ msgstr "Bezahlstatus"
msgid "Reset"
msgstr "Zurücksetzen"
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid " (Field: %{field})"
msgstr " (Datenfeld: %{field})"
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "CSV File"
msgstr "CSV Datei"
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Download CSV templates:"
msgstr "CSV Vorlagen herunterladen:"
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "English Template"
msgstr "Englische Vorlage"
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Error list truncated to %{count} entries"
msgstr "Liste der Fehler auf %{count} Einträge reduziert"
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Errors"
msgstr "Fehler"
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format
msgid "Failed to prepare CSV import: %{reason}"
msgstr "Das Vorbereiten des CSV Imports ist gescheitert: %{reason}"
@ -2025,27 +2027,27 @@ msgstr "Das Importieren von %{idx} ist gescheitert: %{reason}"
msgid "Failed to read file: %{reason}"
msgstr "Fehler beim Lesen der Datei: %{reason}"
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Failed: %{count} row(s)"
msgstr "Fehlgeschlagen: %{count} Zeile(n)"
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "German Template"
msgstr "Deutsche Vorlage"
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format
msgid "Import Members (CSV)"
msgstr "Mitglieder importieren (CSV)"
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Import Results"
msgstr "Import-Ergebnisse"
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format
msgid "Import is already running. Please wait for it to complete."
msgstr "Import läuft bereits. Bitte warte, bis er abgeschlossen ist."
@ -2060,7 +2062,7 @@ msgstr "Import-Status fehlt. Chunk %{idx} kann nicht verarbeitet werden."
msgid "Invalid chunk index: %{idx}"
msgstr "Ungültiger Chunk-Index: %{idx}"
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Line %{line}: %{message}"
msgstr "Zeile %{line}: %{message}"
@ -2070,47 +2072,47 @@ msgstr "Zeile %{line}: %{message}"
msgid "No file was uploaded"
msgstr "Es wurde keine Datei hochgeladen"
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format
msgid "Only administrators can import members from CSV files."
msgstr "Nur Administrator*innen können Mitglieder aus CSV-Dateien importieren."
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format
msgid "Please select a CSV file to import."
msgstr "Bitte wähle eine CSV-Datei zum Importieren."
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format
msgid "Please wait for the file upload to complete before starting the import."
msgstr "Bitte warte, bis der Datei-Upload abgeschlossen ist, bevor du den Import startest."
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Processing chunk %{current} of %{total}..."
msgstr "Verarbeite Chunk %{current} von %{total}..."
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Start Import"
msgstr "Import starten"
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Starting import..."
msgstr "Import wird gestartet..."
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Successfully inserted: %{count} member(s)"
msgstr "Erfolgreich eingefügt: %{count} Mitglied(er)"
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Summary"
msgstr "Zusammenfassung"
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Warnings"
msgstr "Warnungen"
@ -2202,11 +2204,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"
@ -2325,7 +2329,7 @@ msgstr "%{name} entfernen"
msgid "Some members could not be added: %{errors}"
msgstr "Einige Mitglieder konnten nicht hinzugefügt werden: %{errors}"
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "CSV files only, maximum %{size} MB"
msgstr "Nur CSV Dateien, maximal %{size} MB"
@ -2355,43 +2359,22 @@ msgstr "Datenfeld: %{name} erwartet %{type}, erhalten: %{value}"
msgid "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing."
msgstr "Unbekannte Spalte '%{header}' wird ignoriert. Falls dies ein Datenfeld ist, erstelle es in Mila vor dem Import."
#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Export Members (CSV)"
msgstr "Mitglieder importieren (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/membership/import/import_runner.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Failed to read uploaded file: unexpected format"
msgstr "Fehler beim Lesen der hochgeladenen Datei"
#: 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/components/layouts/sidebar.ex
#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Import/Export"
msgstr "Import/Export"
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "You do not have permission to access this page."
msgstr "Du hast keine Berechtigung, auf diese Seite zuzugreifen."
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Manage Member Data"
msgstr "Mitgliederdaten verwalten"
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, so they must be listed in the list of member data (like e-mail or first name). Unknown data field columns will be ignored with a warning."
msgstr "Verwende die Namen der Datenfelder als Spaltennamen in der CSV Datei. Datenfelder müssen in Mila bereits angelegt sein, damit sie importiert werden können. sie müssen in der Liste der Mitgliederdaten als Datenfeld enthalten sein (z.B. E-Mail). Spalten mit unbekannten Spaltenüberschriften werden mit einer Warnung ignoriert."
@ -2453,7 +2436,7 @@ msgstr "SSO / OIDC Nutzer*in"
msgid "This user is linked via SSO (Single Sign-On). A password set or changed here only affects login with email and password in this application. It does not change the password in your identity provider (e.g. Authentik). To change the SSO password, use the identity provider or your organization's IT."
msgstr "Diese*r Nutzer*in ist über SSO (Single Sign-On) verbunden. Ein hier festgelegtes oder geändertes Passwort wirkt sich nur auf die Anmeldung mit E-Mail-Adresse und Passwort in dieser Anwendung aus. Es ändert nicht das Passwort in Eurem Identitätsanbieter (z. B. Authentik). Um das SSO-Passwort zu ändern, wende dich an den Identitätsanbieter oder die IT-Abteilung Ihrer Organisation."
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Import aborted"
msgstr "Import abgebrochen"
@ -2474,6 +2457,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}"
@ -2614,11 +2598,23 @@ msgstr "Anzahl Mitglieder:"
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"
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Import"
msgstr "Import"
#~ #: 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 "Failed to prepare CSV import: %{error}"
#~ msgstr "Das Vorbereiten des CSV Imports ist gescheitert: %{error}"
#~ 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."

View file

@ -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"
@ -1860,7 +1862,7 @@ msgstr ""
msgid "updated"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format
msgid "Unknown error"
@ -1981,37 +1983,37 @@ msgstr ""
msgid "Reset"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid " (Field: %{field})"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "CSV File"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Download CSV templates:"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "English Template"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Error list truncated to %{count} entries"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Errors"
msgstr ""
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format
msgid "Failed to prepare CSV import: %{reason}"
msgstr ""
@ -2026,27 +2028,27 @@ msgstr ""
msgid "Failed to read file: %{reason}"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Failed: %{count} row(s)"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "German Template"
msgstr ""
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format
msgid "Import Members (CSV)"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Import Results"
msgstr ""
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format
msgid "Import is already running. Please wait for it to complete."
msgstr ""
@ -2061,7 +2063,7 @@ msgstr ""
msgid "Invalid chunk index: %{idx}"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Line %{line}: %{message}"
msgstr ""
@ -2071,47 +2073,47 @@ msgstr ""
msgid "No file was uploaded"
msgstr ""
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format
msgid "Only administrators can import members from CSV files."
msgstr ""
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format
msgid "Please select a CSV file to import."
msgstr ""
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format
msgid "Please wait for the file upload to complete before starting the import."
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Processing chunk %{current} of %{total}..."
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Start Import"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Starting import..."
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Successfully inserted: %{count} member(s)"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Summary"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Warnings"
msgstr ""
@ -2203,11 +2205,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 ""
@ -2326,7 +2330,7 @@ msgstr ""
msgid "Some members could not be added: %{errors}"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "CSV files only, maximum %{size} MB"
msgstr ""
@ -2356,43 +2360,22 @@ msgstr ""
msgid "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing."
msgstr ""
#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
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/membership/import/import_runner.ex
#, elixir-autogen, elixir-format
msgid "Failed to read uploaded file: unexpected format"
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/components/layouts/sidebar.ex
#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Import/Export"
msgstr ""
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format
msgid "You do not have permission to access this page."
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Manage Member Data"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, so they must be listed in the list of member data (like e-mail or first name). Unknown data field columns will be ignored with a warning."
msgstr ""
@ -2454,7 +2437,7 @@ msgstr ""
msgid "This user is linked via SSO (Single Sign-On). A password set or changed here only affects login with email and password in this application. It does not change the password in your identity provider (e.g. Authentik). To change the SSO password, use the identity provider or your organization's IT."
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Import aborted"
msgstr ""
@ -2475,6 +2458,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 ""
@ -2614,3 +2598,9 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "PDF"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format
msgid "Import"
msgstr ""

View file

@ -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"
@ -1860,7 +1862,7 @@ msgstr ""
msgid "updated"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format
msgid "Unknown error"
@ -1981,37 +1983,37 @@ msgstr ""
msgid "Reset"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid " (Field: %{field})"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "CSV File"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Download CSV templates:"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "English Template"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Error list truncated to %{count} entries"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Errors"
msgstr ""
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format
msgid "Failed to prepare CSV import: %{reason}"
msgstr ""
@ -2026,27 +2028,27 @@ msgstr ""
msgid "Failed to read file: %{reason}"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Failed: %{count} row(s)"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "German Template"
msgstr ""
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format
msgid "Import Members (CSV)"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Import Results"
msgstr ""
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format
msgid "Import is already running. Please wait for it to complete."
msgstr ""
@ -2061,7 +2063,7 @@ msgstr ""
msgid "Invalid chunk index: %{idx}"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Line %{line}: %{message}"
msgstr ""
@ -2071,47 +2073,47 @@ msgstr ""
msgid "No file was uploaded"
msgstr ""
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format
msgid "Only administrators can import members from CSV files."
msgstr ""
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Please select a CSV file to import."
msgstr ""
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format
msgid "Please wait for the file upload to complete before starting the import."
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Processing chunk %{current} of %{total}..."
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Start Import"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Starting import..."
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Successfully inserted: %{count} member(s)"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Summary"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Warnings"
msgstr ""
@ -2203,11 +2205,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 ""
@ -2326,7 +2330,7 @@ msgstr ""
msgid "Some members could not be added: %{errors}"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "CSV files only, maximum %{size} MB"
msgstr ""
@ -2356,43 +2360,22 @@ msgstr ""
msgid "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing."
msgstr "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing."
#: 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/membership/import/import_runner.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Failed to read uploaded file: unexpected format"
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/components/layouts/sidebar.ex
#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Import/Export"
msgstr ""
#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "You do not have permission to access this page."
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Manage Member Data"
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, so they must be listed in the list of member data (like e-mail or first name). Unknown data field columns will be ignored with a warning."
msgstr ""
@ -2454,7 +2437,7 @@ msgstr ""
msgid "This user is linked via SSO (Single Sign-On). A password set or changed here only affects login with email and password in this application. It does not change the password in your identity provider (e.g. Authentik). To change the SSO password, use the identity provider or your organization's IT."
msgstr ""
#: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format
msgid "Import aborted"
msgstr ""
@ -2475,6 +2458,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 ""
@ -2615,7 +2599,23 @@ msgstr "Member count:"
msgid "PDF"
msgstr ""
#~ #: lib/mv_web/live/global_settings_live.ex
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/import_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Import"
msgstr ""
#~ #: lib/mv_web/live/import_export_live.ex
#~ #, elixir-autogen, elixir-format, fuzzy
#~ msgid "Custom Fields in CSV Import"
#~ 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 ""

View file

@ -42,7 +42,7 @@ defmodule MvWeb.GlobalSettingsLiveConfigTest do
# Arrange: Set custom row limit to 500
Application.put_env(:mv, :csv_import, max_rows: 500)
{:ok, view, _html} = live(conn, ~p"/admin/import-export")
{:ok, view, _html} = live(conn, ~p"/admin/import")
# Generate CSV with 501 rows (exceeding custom limit of 500)
header = "first_name;last_name;email;street;postal_code;city\n"

View file

@ -1,6 +1,6 @@
defmodule MvWeb.ImportExportLiveTest do
defmodule MvWeb.ImportLiveTest do
@moduledoc """
Tests for Import/Export LiveView: authorization (business rule), CSV import integration,
Tests for Import LiveView: authorization (business rule), CSV import integration,
and minimal UI smoke tests. CSV parsing/validation logic is covered by
Mv.Membership.Import.MemberCSVTest; here we verify access control and end-to-end outcomes.
"""
@ -31,7 +31,7 @@ defmodule MvWeb.ImportExportLiveTest do
# ---------- Business logic: Authorization ----------
describe "Authorization" do
test "non-admin user cannot access import/export page and sees permission error", %{
test "non-admin user cannot access import page and sees permission error", %{
conn: conn
} do
member_user = Mv.Fixtures.user_with_role_fixture("own_data")
@ -42,7 +42,7 @@ defmodule MvWeb.ImportExportLiveTest do
|> put_locale_en()
assert {:error, {:redirect, %{to: redirect_path, flash: %{"error" => msg}}}} =
live(conn, ~p"/admin/import-export")
live(conn, ~p"/admin/import")
assert redirect_path =~ "/users/"
assert msg =~ "don't have permission"
@ -55,7 +55,7 @@ defmodule MvWeb.ImportExportLiveTest do
Path.join([__DIR__, "..", "..", "fixtures", "valid_member_import.csv"])
|> File.read!()
{:ok, view, _html} = live(conn, ~p"/admin/import-export")
{:ok, view, _html} = live(conn, ~p"/admin/import")
upload_csv_file(view, csv_content)
submit_import(view)
wait_for_import_completion()
@ -109,7 +109,7 @@ defmodule MvWeb.ImportExportLiveTest do
end
test "invalid CSV shows user-friendly prepare error", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/import-export")
{:ok, view, _html} = live(conn, ~p"/admin/import")
upload_csv_file(view, "invalid_header\nincomplete_row", "invalid.csv")
submit_import(view)
html = render(view)
@ -120,7 +120,7 @@ defmodule MvWeb.ImportExportLiveTest do
conn: conn,
invalid_csv: csv_content
} do
{:ok, view, _html} = live(conn, ~p"/admin/import-export")
{:ok, view, _html} = live(conn, ~p"/admin/import")
upload_csv_file(view, csv_content, "invalid_import.csv")
submit_import(view)
wait_for_import_completion()
@ -135,7 +135,7 @@ defmodule MvWeb.ImportExportLiveTest do
end
test "error list is capped and truncation message is shown", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/import-export")
{:ok, view, _html} = live(conn, ~p"/admin/import")
header = "first_name;last_name;email;street;postal_code;city\n"
invalid_rows =
@ -153,7 +153,7 @@ defmodule MvWeb.ImportExportLiveTest do
end
test "row limit is enforced (1001 rows rejected)", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/import-export")
{:ok, view, _html} = live(conn, ~p"/admin/import")
header = "first_name;last_name;email;street;postal_code;city\n"
rows =
@ -168,7 +168,7 @@ defmodule MvWeb.ImportExportLiveTest do
end
test "BOM and semicolon delimiter are accepted", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/import-export")
{:ok, view, _html} = live(conn, ~p"/admin/import")
csv_content =
Path.join([__DIR__, "..", "..", "fixtures", "csv_with_bom_semicolon.csv"])
@ -187,7 +187,7 @@ defmodule MvWeb.ImportExportLiveTest do
test "physical line numbers in errors (empty line does not shift numbering)", %{
conn: conn
} do
{:ok, view, _html} = live(conn, ~p"/admin/import-export")
{:ok, view, _html} = live(conn, ~p"/admin/import")
csv_content =
Path.join([__DIR__, "..", "..", "fixtures", "csv_with_empty_lines.csv"])
@ -207,7 +207,7 @@ defmodule MvWeb.ImportExportLiveTest do
conn: conn,
unknown_custom_field_csv: csv_content
} do
{:ok, view, _html} = live(conn, ~p"/admin/import-export")
{:ok, view, _html} = live(conn, ~p"/admin/import")
upload_csv_file(view, csv_content, "unknown_custom.csv")
submit_import(view)
wait_for_import_completion()
@ -220,7 +220,7 @@ defmodule MvWeb.ImportExportLiveTest do
end
# ---------- UI (smoke / framework): tagged for exclusion from fast CI ----------
describe "Import/Export page UI" do
describe "Import page UI" do
@describetag :ui
setup %{conn: conn} do
admin_user = Mv.Fixtures.user_with_role_fixture("admin")
@ -233,19 +233,17 @@ defmodule MvWeb.ImportExportLiveTest do
{:ok, conn: conn}
end
test "page loads and shows import form and export placeholder", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/import-export")
test "page loads and shows import form", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/import")
assert has_element?(view, "[data-testid='csv-upload-form']")
assert has_element?(view, "[data-testid='start-import-button']")
assert has_element?(view, "[data-testid='custom-fields-link']")
html = render(view)
assert html =~ "Import Members (CSV)"
assert html =~ "Export Members (CSV)"
assert html =~ "Export functionality will be available"
end
test "template links and file input are present", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/import-export")
{:ok, view, _html} = live(conn, ~p"/admin/import")
assert has_element?(view, "a[href*='/templates/member_import_en.csv']")
assert has_element?(view, "a[href*='/templates/member_import_de.csv']")
assert has_element?(view, "label[for='csv_file']")
@ -258,7 +256,7 @@ defmodule MvWeb.ImportExportLiveTest do
Path.join([__DIR__, "..", "..", "fixtures", "valid_member_import.csv"])
|> File.read!()
{:ok, view, _html} = live(conn, ~p"/admin/import-export")
{:ok, view, _html} = live(conn, ~p"/admin/import")
upload_csv_file(view, csv_content)
submit_import(view)
wait_for_import_completion()
@ -273,7 +271,7 @@ defmodule MvWeb.ImportExportLiveTest do
@tag :skip
test "empty CSV shows error", %{conn: conn} do
conn = put_locale_en(conn)
{:ok, view, _html} = live(conn, ~p"/admin/import-export")
{:ok, view, _html} = live(conn, ~p"/admin/import")
upload_csv_file(view, " ", "empty.csv")
submit_import(view)
html = render(view)

View file

@ -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

View file

@ -0,0 +1,262 @@
defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do
@moduledoc """
Tests for displaying groups in the member detail view (Issue #374).
Tests cover:
- Groups in Personal Data (with and without groups)
- 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 aria-label for screen readers
## Note on async
async: false to avoid PostgreSQL deadlocks when creating members and groups
in the same test run (same as IndexGroupsDisplayTest).
"""
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
actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, member} =
create_member(actor, %{
first_name: "Alice",
last_name: "Anderson",
email: "alice@example.com"
})
%{member: member, actor: actor}
end
test "displays Groups section when member has at least one group", %{
conn: conn,
member: member,
actor: actor
} do
{: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}")
assert html =~ gettext("Groups")
assert html =~ group.name
end
test "displays all group links with correct names when member is in multiple groups", %{
conn: conn,
member: member,
actor: actor
} do
{: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}")
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}")
# 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 "renders all group names when member has multiple groups", %{
conn: conn,
member: member,
actor: actor
} do
{: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}")
assert html =~ "Alpha"
assert html =~ "Beta"
end
end
describe "groups section links" do
setup do
actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, member} =
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 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}")
# Link to group detail: /groups/:slug (slug is URL-friendly, e.g. "board-members")
assert has_element?(view, "a[href*='/groups/#{group.slug}']", group.name)
end
test "clicking group link 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
actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, member} =
create_member(actor, %{
first_name: "Charlie",
last_name: "Clark",
email: "charlie@example.com"
})
%{member: member, actor: actor}
end
test "member in exactly one group shows single link", %{
conn: conn,
member: member,
actor: actor
} do
{: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}")
assert html =~ gettext("Groups")
assert html =~ group.name
end
test "member in many groups shows all group links", %{
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} = create_group(actor, name)
g
end)
for g <- groups do
{:ok, _mg} = add_member_to_group(member, g, 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
actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, member} =
create_member(actor, %{
first_name: "Diana",
last_name: "Davis",
email: "diana@example.com"
})
{:ok, group} = create_group(actor, "Readers")
{:ok, _mg} = add_member_to_group(member, group, 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
actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, member} =
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 links have 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
# 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