Compare commits

..

1 commit

Author SHA1 Message Date
Renovate Bot
cbadaa8b14 chore(deps): update renovate/renovate docker tag to v43
Some checks reported errors
continuous-integration/drone/push Build was killed
2026-02-17 00:16:28 +00:00
17 changed files with 256 additions and 537 deletions

View file

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

View file

@ -356,9 +356,9 @@ lib/
- Screen readers must be able to navigate and understand the interface - Screen readers must be able to navigate and understand the interface
- ARIA labels and roles must be properly set - ARIA labels and roles must be properly set
**Group Badges and Links in Member Overview / Detail:** **Group Badges in Member Overview:**
- 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. - Badges must have `role="status"` and appropriate `aria-label` attributes
- Badge/link text or title should indicate group membership for screen readers. - Badge title should indicate group membership
**Clickable Group Badge (for filtering) - Optional:** **Clickable Group Badge (for filtering) - Optional:**
@ -961,8 +961,6 @@ Each functional unit can be implemented as a **separate issue**:
### Issue 4: Member Detail - Groups Display ### Issue 4: Member Detail - Groups Display
**Type:** Frontend **Type:** Frontend
**Estimation:** 1-2h **Estimation:** 1-2h
**Status:** Implemented (Groups as data field in Personal Data, below Linked User; button-style links to `/groups/:slug`).
**Tasks:** **Tasks:**
- Add groups section to member show - Add groups section to member show
- Display group badges - Display group badges

View file

@ -1,7 +1,7 @@
defmodule Mv.Membership.Import.ImportRunner do defmodule Mv.Membership.Import.ImportRunner do
@moduledoc """ @moduledoc """
Orchestrates CSV member import: file reading, progress tracking, chunk processing, Orchestrates CSV member import: file reading, progress tracking, chunk processing,
and error formatting. Used by `MvWeb.ImportLive` to keep LiveView thin. and error formatting. Used by `MvWeb.ImportExportLive` to keep LiveView thin.
This module does not depend on Phoenix or LiveView. It provides pure functions for 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 progress/merge and side-effectful helpers (read_file_entry, process_chunk) that

View file

@ -118,7 +118,7 @@ defmodule MvWeb.Layouts.Sidebar do
/> />
<% end %> <% end %>
<%= if can_access_page?(@current_user, PagePaths.settings()) do %> <%= if can_access_page?(@current_user, PagePaths.settings()) do %>
<.menu_subitem href={~p"/admin/import"} label={gettext("Import")} /> <.menu_subitem href={~p"/admin/import-export"} label={gettext("Import/Export")} />
<.menu_subitem href={~p"/settings"} label={gettext("Settings")} /> <.menu_subitem href={~p"/settings"} label={gettext("Settings")} />
<% end %> <% end %>
</.menu_group> </.menu_group>

View file

@ -219,9 +219,6 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
send(self(), {:editing_section_changed, nil}) send(self(), {:editing_section_changed, nil})
end end
# Get actor from assigns or fall back to socket assigns
actor = Map.get(assigns, :actor, socket.assigns[:actor])
{:ok, {:ok,
socket socket
|> assign(assigns) |> assign(assigns)
@ -231,7 +228,7 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
|> assign_new(:show_delete_modal, fn -> false end) |> assign_new(:show_delete_modal, fn -> false end)
|> assign_new(:custom_field_to_delete, fn -> nil end) |> assign_new(:custom_field_to_delete, fn -> nil end)
|> assign_new(:slug_confirmation, fn -> "" end) |> assign_new(:slug_confirmation, fn -> "" end)
|> stream(:custom_fields, stream_custom_fields(actor, self()), reset: true)} |> stream(:custom_fields, stream_custom_fields(assigns[:actor], self()), reset: true)}
end end
@impl true @impl true

View file

@ -1,6 +1,6 @@
defmodule MvWeb.ImportLive do defmodule MvWeb.ImportExportLive do
@moduledoc """ @moduledoc """
LiveView for importing members via CSV. LiveView for importing and exporting members via CSV.
## Features ## Features
- CSV member import (admin only) - CSV member import (admin only)
@ -38,7 +38,7 @@ defmodule MvWeb.ImportLive do
alias Mv.Membership.Import.ImportRunner alias Mv.Membership.Import.ImportRunner
alias Mv.Membership.Import.MemberCSV alias Mv.Membership.Import.MemberCSV
alias MvWeb.Authorization alias MvWeb.Authorization
alias MvWeb.ImportLive.Components alias MvWeb.ImportExportLive.Components
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded} on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
@ -65,7 +65,7 @@ defmodule MvWeb.ImportLive do
socket = socket =
socket socket
|> assign(:page_title, gettext("Import")) |> assign(:page_title, gettext("Import/Export"))
|> assign(:club_name, club_name) |> assign(:club_name, club_name)
|> assign(:import_state, nil) |> assign(:import_state, nil)
|> assign(:import_progress, nil) |> assign(:import_progress, nil)
@ -90,6 +90,13 @@ defmodule MvWeb.ImportLive do
def render(assigns) do def render(assigns) do
~H""" ~H"""
<Layouts.app flash={@flash} current_user={@current_user} club_name={@club_name}> <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 %> <%= if Authorization.can?(@current_user, :create, Mv.Membership.Member) do %>
<%!-- CSV Import Section --%> <%!-- CSV Import Section --%>
<.form_section title={gettext("Import Members (CSV)")}> <.form_section title={gettext("Import Members (CSV)")}>
@ -100,6 +107,18 @@ defmodule MvWeb.ImportLive do
<Components.import_progress {assigns} /> <Components.import_progress {assigns} />
<% end %> <% end %>
</.form_section> </.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 %> <% else %>
<div role="alert" class="alert alert-error"> <div role="alert" class="alert alert-error">
<.icon name="hero-exclamation-circle" class="size-5" aria-hidden="true" /> <.icon name="hero-exclamation-circle" class="size-5" aria-hidden="true" />

View file

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

View file

@ -180,67 +180,6 @@
> >
{member.email} {member.email}
</:col> </: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 <:col
:let={member} :let={member}
:if={:street in @member_fields_visible} :if={:street in @member_fields_visible}
@ -297,21 +236,57 @@
</:col> </:col>
<:col <:col
:let={member} :let={member}
:if={:membership_fee_start_date in @member_fields_visible} :if={:city in @member_fields_visible}
label={ label={
~H""" ~H"""
<.live_component <.live_component
module={MvWeb.Components.SortHeaderComponent} module={MvWeb.Components.SortHeaderComponent}
id={:sort_membership_fee_start_date} id={:sort_city}
field={:membership_fee_start_date} field={:city}
label={gettext("Membership Fee Start Date")} label={gettext("City")}
sort_field={@sort_field} sort_field={@sort_field}
sort_order={@sort_order} sort_order={@sort_order}
/> />
""" """
} }
> >
{MvWeb.MemberLive.Index.format_date(member.membership_fee_start_date)} {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)}
</:col> </:col>
<:col <:col
:let={member} :let={member}
@ -347,6 +322,7 @@
<%= for group <- (member.groups || []) do %> <%= for group <- (member.groups || []) do %>
<span <span
class="badge badge-outline badge-primary" class="badge badge-outline badge-primary"
role="status"
aria-label={gettext("Member of group %{name}", name: group.name)} aria-label={gettext("Member of group %{name}", name: group.name)}
> >
{group.name} {group.name}

View file

@ -12,8 +12,6 @@ defmodule MvWeb.MemberLive.Show do
## Sections ## Sections
- Personal Data: Name, address, contact information, membership dates, notes - Personal Data: Name, address, contact information, membership dates, notes
- Custom Fields: Dynamic fields in uniform grid layout (sorted by name) - Custom Fields: Dynamic fields in uniform grid layout (sorted by name)
- Groups: 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) - Membership Fees: Tab showing all membership fee cycles with status management (via MembershipFeesComponent)
## Navigation ## Navigation
@ -148,28 +146,6 @@ defmodule MvWeb.MemberLive.Show do
</div> </div>
<% end %> <% 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 --%> <%!-- Notes --%>
<%= if @member.notes && String.trim(@member.notes) != "" do %> <%= if @member.notes && String.trim(@member.notes) != "" do %>
<div> <div>
@ -286,8 +262,7 @@ defmodule MvWeb.MemberLive.Show do
:user, :user,
:membership_fee_type, :membership_fee_type,
custom_field_values: [:custom_field], custom_field_values: [:custom_field],
membership_fee_cycles: [:membership_fee_type], membership_fee_cycles: [:membership_fee_type]
groups: [:id, :name, :slug]
]) ])
member = Ash.read_one!(query, actor: actor) member = Ash.read_one!(query, actor: actor)

View file

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

View file

@ -144,7 +144,6 @@ msgid "House Number"
msgstr "Hausnummer" msgstr "Hausnummer"
#: lib/mv_web/live/member_live/form.ex #: 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/live/member_live/show.ex
#: lib/mv_web/translations/member_fields.ex #: lib/mv_web/translations/member_fields.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -1543,7 +1542,6 @@ msgstr "Du bist dabei alle %{count} Zyklen für dieses Mitglied zu löschen."
msgid "Delete Membership Fee Type" msgid "Delete Membership Fee Type"
msgstr "Mitgliedsbeitragsart löschen" msgstr "Mitgliedsbeitragsart löschen"
#: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/translations/member_fields.ex #: lib/mv_web/translations/member_fields.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Membership Fee Start Date" msgid "Membership Fee Start Date"
@ -1861,7 +1859,7 @@ msgstr "erstellt"
msgid "updated" msgid "updated"
msgstr "aktualisiert" msgstr "aktualisiert"
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/user_live/form.ex #: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unknown error" msgid "Unknown error"
@ -1982,37 +1980,37 @@ msgstr "Bezahlstatus"
msgid "Reset" msgid "Reset"
msgstr "Zurücksetzen" msgstr "Zurücksetzen"
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid " (Field: %{field})" msgid " (Field: %{field})"
msgstr " (Datenfeld: %{field})" msgstr " (Datenfeld: %{field})"
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "CSV File" msgid "CSV File"
msgstr "CSV Datei" msgstr "CSV Datei"
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Download CSV templates:" msgid "Download CSV templates:"
msgstr "CSV Vorlagen herunterladen:" msgstr "CSV Vorlagen herunterladen:"
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "English Template" msgid "English Template"
msgstr "Englische Vorlage" msgstr "Englische Vorlage"
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Error list truncated to %{count} entries" msgid "Error list truncated to %{count} entries"
msgstr "Liste der Fehler auf %{count} Einträge reduziert" msgstr "Liste der Fehler auf %{count} Einträge reduziert"
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Errors" msgid "Errors"
msgstr "Fehler" msgstr "Fehler"
#: lib/mv_web/live/import_live.ex #: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Failed to prepare CSV import: %{reason}" msgid "Failed to prepare CSV import: %{reason}"
msgstr "Das Vorbereiten des CSV Imports ist gescheitert: %{reason}" msgstr "Das Vorbereiten des CSV Imports ist gescheitert: %{reason}"
@ -2027,27 +2025,27 @@ msgstr "Das Importieren von %{idx} ist gescheitert: %{reason}"
msgid "Failed to read file: %{reason}" msgid "Failed to read file: %{reason}"
msgstr "Fehler beim Lesen der Datei: %{reason}" msgstr "Fehler beim Lesen der Datei: %{reason}"
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Failed: %{count} row(s)" msgid "Failed: %{count} row(s)"
msgstr "Fehlgeschlagen: %{count} Zeile(n)" msgstr "Fehlgeschlagen: %{count} Zeile(n)"
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "German Template" msgid "German Template"
msgstr "Deutsche Vorlage" msgstr "Deutsche Vorlage"
#: lib/mv_web/live/import_live.ex #: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import Members (CSV)" msgid "Import Members (CSV)"
msgstr "Mitglieder importieren (CSV)" msgstr "Mitglieder importieren (CSV)"
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import Results" msgid "Import Results"
msgstr "Import-Ergebnisse" msgstr "Import-Ergebnisse"
#: lib/mv_web/live/import_live.ex #: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import is already running. Please wait for it to complete." msgid "Import is already running. Please wait for it to complete."
msgstr "Import läuft bereits. Bitte warte, bis er abgeschlossen ist." msgstr "Import läuft bereits. Bitte warte, bis er abgeschlossen ist."
@ -2062,7 +2060,7 @@ msgstr "Import-Status fehlt. Chunk %{idx} kann nicht verarbeitet werden."
msgid "Invalid chunk index: %{idx}" msgid "Invalid chunk index: %{idx}"
msgstr "Ungültiger Chunk-Index: %{idx}" msgstr "Ungültiger Chunk-Index: %{idx}"
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Line %{line}: %{message}" msgid "Line %{line}: %{message}"
msgstr "Zeile %{line}: %{message}" msgstr "Zeile %{line}: %{message}"
@ -2072,47 +2070,47 @@ msgstr "Zeile %{line}: %{message}"
msgid "No file was uploaded" msgid "No file was uploaded"
msgstr "Es wurde keine Datei hochgeladen" msgstr "Es wurde keine Datei hochgeladen"
#: lib/mv_web/live/import_live.ex #: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Only administrators can import members from CSV files." msgid "Only administrators can import members from CSV files."
msgstr "Nur Administrator*innen können Mitglieder aus CSV-Dateien importieren." msgstr "Nur Administrator*innen können Mitglieder aus CSV-Dateien importieren."
#: lib/mv_web/live/import_live.ex #: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Please select a CSV file to import." msgid "Please select a CSV file to import."
msgstr "Bitte wähle eine CSV-Datei zum Importieren." msgstr "Bitte wähle eine CSV-Datei zum Importieren."
#: lib/mv_web/live/import_live.ex #: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Please wait for the file upload to complete before starting the import." 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." msgstr "Bitte warte, bis der Datei-Upload abgeschlossen ist, bevor du den Import startest."
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Processing chunk %{current} of %{total}..." msgid "Processing chunk %{current} of %{total}..."
msgstr "Verarbeite Chunk %{current} von %{total}..." msgstr "Verarbeite Chunk %{current} von %{total}..."
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Start Import" msgid "Start Import"
msgstr "Import starten" msgstr "Import starten"
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Starting import..." msgid "Starting import..."
msgstr "Import wird gestartet..." msgstr "Import wird gestartet..."
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Successfully inserted: %{count} member(s)" msgid "Successfully inserted: %{count} member(s)"
msgstr "Erfolgreich eingefügt: %{count} Mitglied(er)" msgstr "Erfolgreich eingefügt: %{count} Mitglied(er)"
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Summary" msgid "Summary"
msgstr "Zusammenfassung" msgstr "Zusammenfassung"
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Warnings" msgid "Warnings"
msgstr "Warnungen" msgstr "Warnungen"
@ -2204,13 +2202,11 @@ msgstr "Gruppe erfolgreich gespeichert."
#: lib/mv_web/live/components/member_filter_component.ex #: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Groups" msgid "Groups"
msgstr "Gruppen" msgstr "Gruppen"
#: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No groups" msgid "No groups"
msgstr "Keine Gruppen" msgstr "Keine Gruppen"
@ -2329,7 +2325,7 @@ msgstr "%{name} entfernen"
msgid "Some members could not be added: %{errors}" msgid "Some members could not be added: %{errors}"
msgstr "Einige Mitglieder konnten nicht hinzugefügt werden: %{errors}" msgstr "Einige Mitglieder konnten nicht hinzugefügt werden: %{errors}"
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "CSV files only, maximum %{size} MB" msgid "CSV files only, maximum %{size} MB"
msgstr "Nur CSV Dateien, maximal %{size} MB" msgstr "Nur CSV Dateien, maximal %{size} MB"
@ -2359,22 +2355,43 @@ 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." 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." 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 #: lib/mv/membership/import/import_runner.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Failed to read uploaded file: unexpected format" msgid "Failed to read uploaded file: unexpected format"
msgstr "Fehler beim Lesen der hochgeladenen Datei" msgstr "Fehler beim Lesen der hochgeladenen Datei"
#: lib/mv_web/live/import_live.ex #: 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
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "You do not have permission to access this page." msgid "You do not have permission to access this page."
msgstr "Du hast keine Berechtigung, auf diese Seite zuzugreifen." msgstr "Du hast keine Berechtigung, auf diese Seite zuzugreifen."
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Manage Member Data" msgid "Manage Member Data"
msgstr "Mitgliederdaten verwalten" msgstr "Mitgliederdaten verwalten"
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format, fuzzy #, 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." 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." 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."
@ -2436,7 +2453,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." 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." 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_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import aborted" msgid "Import aborted"
msgstr "Import abgebrochen" msgstr "Import abgebrochen"
@ -2457,7 +2474,6 @@ msgid "unpaid"
msgstr "Unbezahlt" msgstr "Unbezahlt"
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Member of group %{name}" msgid "Member of group %{name}"
msgstr "Mitglied der Gruppe %{name}" msgstr "Mitglied der Gruppe %{name}"
@ -2598,23 +2614,11 @@ msgstr "Anzahl Mitglieder:"
msgid "PDF" msgid "PDF"
msgstr "PDF" msgstr "PDF"
#: lib/mv_web/components/layouts/sidebar.ex #~ #: lib/mv_web/live/global_settings_live.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 #~ #, elixir-autogen, elixir-format, fuzzy
#~ msgid "Export Members (CSV)" #~ msgid "Custom Fields in CSV Import"
#~ msgstr "Mitglieder exportieren (CSV)" #~ msgstr "Benutzerdefinierte Felder"
#~ #: lib/mv_web/live/import_export_live.ex
#~ #, elixir-autogen, elixir-format #~ #, elixir-autogen, elixir-format
#~ msgid "Export functionality will be available in a future release." #~ msgid "Failed to prepare CSV import: %{error}"
#~ msgstr "Export-Funktionalität ist im nächsten release verfügbar." #~ msgstr "Das Vorbereiten des CSV Imports ist gescheitert: %{error}"
#~ #: 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,7 +145,6 @@ msgid "House Number"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex #: 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/live/member_live/show.ex
#: lib/mv_web/translations/member_fields.ex #: lib/mv_web/translations/member_fields.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -1544,7 +1543,6 @@ msgstr ""
msgid "Delete Membership Fee Type" msgid "Delete Membership Fee Type"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/translations/member_fields.ex #: lib/mv_web/translations/member_fields.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Membership Fee Start Date" msgid "Membership Fee Start Date"
@ -1862,7 +1860,7 @@ msgstr ""
msgid "updated" msgid "updated"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/user_live/form.ex #: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unknown error" msgid "Unknown error"
@ -1983,37 +1981,37 @@ msgstr ""
msgid "Reset" msgid "Reset"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid " (Field: %{field})" msgid " (Field: %{field})"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "CSV File" msgid "CSV File"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Download CSV templates:" msgid "Download CSV templates:"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "English Template" msgid "English Template"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Error list truncated to %{count} entries" msgid "Error list truncated to %{count} entries"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Errors" msgid "Errors"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live.ex #: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Failed to prepare CSV import: %{reason}" msgid "Failed to prepare CSV import: %{reason}"
msgstr "" msgstr ""
@ -2028,27 +2026,27 @@ msgstr ""
msgid "Failed to read file: %{reason}" msgid "Failed to read file: %{reason}"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Failed: %{count} row(s)" msgid "Failed: %{count} row(s)"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "German Template" msgid "German Template"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live.ex #: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import Members (CSV)" msgid "Import Members (CSV)"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import Results" msgid "Import Results"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live.ex #: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import is already running. Please wait for it to complete." msgid "Import is already running. Please wait for it to complete."
msgstr "" msgstr ""
@ -2063,7 +2061,7 @@ msgstr ""
msgid "Invalid chunk index: %{idx}" msgid "Invalid chunk index: %{idx}"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Line %{line}: %{message}" msgid "Line %{line}: %{message}"
msgstr "" msgstr ""
@ -2073,47 +2071,47 @@ msgstr ""
msgid "No file was uploaded" msgid "No file was uploaded"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live.ex #: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Only administrators can import members from CSV files." msgid "Only administrators can import members from CSV files."
msgstr "" msgstr ""
#: lib/mv_web/live/import_live.ex #: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Please select a CSV file to import." msgid "Please select a CSV file to import."
msgstr "" msgstr ""
#: lib/mv_web/live/import_live.ex #: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Please wait for the file upload to complete before starting the import." msgid "Please wait for the file upload to complete before starting the import."
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Processing chunk %{current} of %{total}..." msgid "Processing chunk %{current} of %{total}..."
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Start Import" msgid "Start Import"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Starting import..." msgid "Starting import..."
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Successfully inserted: %{count} member(s)" msgid "Successfully inserted: %{count} member(s)"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Summary" msgid "Summary"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Warnings" msgid "Warnings"
msgstr "" msgstr ""
@ -2205,13 +2203,11 @@ msgstr ""
#: lib/mv_web/live/components/member_filter_component.ex #: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Groups" msgid "Groups"
msgstr "" msgstr ""
#: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No groups" msgid "No groups"
msgstr "" msgstr ""
@ -2330,7 +2326,7 @@ msgstr ""
msgid "Some members could not be added: %{errors}" msgid "Some members could not be added: %{errors}"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "CSV files only, maximum %{size} MB" msgid "CSV files only, maximum %{size} MB"
msgstr "" msgstr ""
@ -2360,22 +2356,43 @@ msgstr ""
msgid "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing." msgid "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing."
msgstr "" 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 #: lib/mv/membership/import/import_runner.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Failed to read uploaded file: unexpected format" msgid "Failed to read uploaded file: unexpected format"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live.ex #: 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
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "You do not have permission to access this page." msgid "You do not have permission to access this page."
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Manage Member Data" msgid "Manage Member Data"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, 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." 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 "" msgstr ""
@ -2437,7 +2454,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." 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 "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import aborted" msgid "Import aborted"
msgstr "" msgstr ""
@ -2458,7 +2475,6 @@ msgid "unpaid"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Member of group %{name}" msgid "Member of group %{name}"
msgstr "" msgstr ""
@ -2598,9 +2614,3 @@ msgstr ""
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "PDF" msgid "PDF"
msgstr "" 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,7 +145,6 @@ msgid "House Number"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex #: 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/live/member_live/show.ex
#: lib/mv_web/translations/member_fields.ex #: lib/mv_web/translations/member_fields.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -1544,7 +1543,6 @@ msgstr ""
msgid "Delete Membership Fee Type" msgid "Delete Membership Fee Type"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/translations/member_fields.ex #: lib/mv_web/translations/member_fields.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Membership Fee Start Date" msgid "Membership Fee Start Date"
@ -1862,7 +1860,7 @@ msgstr ""
msgid "updated" msgid "updated"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#: lib/mv_web/live/user_live/form.ex #: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unknown error" msgid "Unknown error"
@ -1983,37 +1981,37 @@ msgstr ""
msgid "Reset" msgid "Reset"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid " (Field: %{field})" msgid " (Field: %{field})"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "CSV File" msgid "CSV File"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Download CSV templates:" msgid "Download CSV templates:"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "English Template" msgid "English Template"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Error list truncated to %{count} entries" msgid "Error list truncated to %{count} entries"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Errors" msgid "Errors"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live.ex #: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Failed to prepare CSV import: %{reason}" msgid "Failed to prepare CSV import: %{reason}"
msgstr "" msgstr ""
@ -2028,27 +2026,27 @@ msgstr ""
msgid "Failed to read file: %{reason}" msgid "Failed to read file: %{reason}"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Failed: %{count} row(s)" msgid "Failed: %{count} row(s)"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "German Template" msgid "German Template"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live.ex #: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import Members (CSV)" msgid "Import Members (CSV)"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import Results" msgid "Import Results"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live.ex #: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import is already running. Please wait for it to complete." msgid "Import is already running. Please wait for it to complete."
msgstr "" msgstr ""
@ -2063,7 +2061,7 @@ msgstr ""
msgid "Invalid chunk index: %{idx}" msgid "Invalid chunk index: %{idx}"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Line %{line}: %{message}" msgid "Line %{line}: %{message}"
msgstr "" msgstr ""
@ -2073,47 +2071,47 @@ msgstr ""
msgid "No file was uploaded" msgid "No file was uploaded"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live.ex #: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Only administrators can import members from CSV files." msgid "Only administrators can import members from CSV files."
msgstr "" msgstr ""
#: lib/mv_web/live/import_live.ex #: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Please select a CSV file to import." msgid "Please select a CSV file to import."
msgstr "" msgstr ""
#: lib/mv_web/live/import_live.ex #: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Please wait for the file upload to complete before starting the import." msgid "Please wait for the file upload to complete before starting the import."
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Processing chunk %{current} of %{total}..." msgid "Processing chunk %{current} of %{total}..."
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Start Import" msgid "Start Import"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Starting import..." msgid "Starting import..."
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Successfully inserted: %{count} member(s)" msgid "Successfully inserted: %{count} member(s)"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Summary" msgid "Summary"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Warnings" msgid "Warnings"
msgstr "" msgstr ""
@ -2205,13 +2203,11 @@ msgstr ""
#: lib/mv_web/live/components/member_filter_component.ex #: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Groups" msgid "Groups"
msgstr "" msgstr ""
#: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No groups" msgid "No groups"
msgstr "" msgstr ""
@ -2330,7 +2326,7 @@ msgstr ""
msgid "Some members could not be added: %{errors}" msgid "Some members could not be added: %{errors}"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "CSV files only, maximum %{size} MB" msgid "CSV files only, maximum %{size} MB"
msgstr "" msgstr ""
@ -2360,22 +2356,43 @@ msgstr ""
msgid "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing." 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." 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 #: lib/mv/membership/import/import_runner.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Failed to read uploaded file: unexpected format" msgid "Failed to read uploaded file: unexpected format"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live.ex #: 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
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "You do not have permission to access this page." msgid "You do not have permission to access this page."
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Manage Member Data" msgid "Manage Member Data"
msgstr "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format, fuzzy #, 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." 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 "" msgstr ""
@ -2437,7 +2454,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." 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 "" msgstr ""
#: lib/mv_web/live/import_live/components.ex #: lib/mv_web/live/import_export_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import aborted" msgid "Import aborted"
msgstr "" msgstr ""
@ -2458,7 +2475,6 @@ msgid "unpaid"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Member of group %{name}" msgid "Member of group %{name}"
msgstr "" msgstr ""
@ -2599,23 +2615,7 @@ msgstr "Member count:"
msgid "PDF" msgid "PDF"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex #~ #: lib/mv_web/live/global_settings_live.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 #~ #, elixir-autogen, elixir-format, fuzzy
#~ msgid "Export Members (CSV)" #~ msgid "Custom Fields in CSV Import"
#~ 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 "" #~ msgstr ""

View file

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

View file

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

View file

@ -1,262 +0,0 @@
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