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: steps:
- name: renovate - name: renovate
image: renovate/renovate:42.97 image: renovate/renovate:43.24
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 in Member Overview:** **Group Badges and Links in Member Overview / Detail:**
- Badges must have `role="status"` and appropriate `aria-label` attributes - 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 title should indicate group membership - Badge/link text or title should indicate group membership for screen readers.
**Clickable Group Badge (for filtering) - Optional:** **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 ### 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.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 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-export"} label={gettext("Import/Export")} /> <.menu_subitem href={~p"/admin/import"} label={gettext("Import")} />
<.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,6 +219,9 @@ 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)
@ -228,7 +231,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(assigns[:actor], self()), reset: true)} |> stream(:custom_fields, stream_custom_fields(actor, self()), reset: true)}
end end
@impl true @impl true

View file

@ -1,6 +1,6 @@
defmodule MvWeb.ImportExportLive do defmodule MvWeb.ImportLive do
@moduledoc """ @moduledoc """
LiveView for importing and exporting members via CSV. LiveView for importing members via CSV.
## Features ## Features
- CSV member import (admin only) - CSV member import (admin only)
@ -38,7 +38,7 @@ defmodule MvWeb.ImportExportLive 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.ImportExportLive.Components alias MvWeb.ImportLive.Components
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded} on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
@ -65,7 +65,7 @@ defmodule MvWeb.ImportExportLive do
socket = socket =
socket socket
|> assign(:page_title, gettext("Import/Export")) |> assign(:page_title, gettext("Import"))
|> 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,13 +90,6 @@ defmodule MvWeb.ImportExportLive 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)")}>
@ -107,18 +100,6 @@ defmodule MvWeb.ImportExportLive 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.ImportExportLive.Components do defmodule MvWeb.ImportLive.Components do
@moduledoc """ @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 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,6 +180,67 @@
> >
{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}
@ -236,57 +297,21 @@
</:col> </:col>
<:col <:col
:let={member} :let={member}
:if={:city in @member_fields_visible} :if={:membership_fee_start_date in @member_fields_visible}
label={ label={
~H""" ~H"""
<.live_component <.live_component
module={MvWeb.Components.SortHeaderComponent} module={MvWeb.Components.SortHeaderComponent}
id={:sort_city} id={:sort_membership_fee_start_date}
field={:city} field={:membership_fee_start_date}
label={gettext("City")} label={gettext("Membership Fee Start Date")}
sort_field={@sort_field} sort_field={@sort_field}
sort_order={@sort_order} sort_order={@sort_order}
/> />
""" """
} }
> >
{member.city} {MvWeb.MemberLive.Index.format_date(member.membership_fee_start_date)}
</: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}
@ -322,7 +347,6 @@
<%= 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,6 +12,8 @@ defmodule MvWeb.MemberLive.Show do
## Sections ## Sections
- Personal Data: Name, address, contact information, membership dates, notes - Personal Data: Name, address, contact information, membership dates, notes
- Custom Fields: Dynamic fields in uniform grid layout (sorted by name) - Custom Fields: Dynamic fields in uniform grid layout (sorted by name)
- Groups: 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
@ -146,6 +148,28 @@ 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>
@ -262,7 +286,8 @@ defmodule MvWeb.MemberLive.Show do
:user, :user,
:membership_fee_type, :membership_fee_type,
custom_field_values: [:custom_field], custom_field_values: [:custom_field],
membership_fee_cycles: [:membership_fee_type] membership_fee_cycles: [:membership_fee_type],
groups: [:id, :name, :slug]
]) ])
member = Ash.read_one!(query, actor: actor) member = Ash.read_one!(query, actor: actor)

View file

@ -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/Export (Admin only) # Import (Admin only)
live "/admin/import-export", ImportExportLive live "/admin/import", ImportLive
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,6 +144,7 @@ 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
@ -1542,6 +1543,7 @@ 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"
@ -1859,7 +1861,7 @@ msgstr "erstellt"
msgid "updated" msgid "updated"
msgstr "aktualisiert" 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 #: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unknown error" msgid "Unknown error"
@ -1980,37 +1982,37 @@ msgstr "Bezahlstatus"
msgid "Reset" msgid "Reset"
msgstr "Zurücksetzen" msgstr "Zurücksetzen"
#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Errors" msgid "Errors"
msgstr "Fehler" msgstr "Fehler"
#: lib/mv_web/live/import_export_live.ex #: lib/mv_web/live/import_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}"
@ -2025,27 +2027,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_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_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_export_live.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_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_export_live.ex #: lib/mv_web/live/import_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."
@ -2060,7 +2062,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_export_live/components.ex #: lib/mv_web/live/import_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}"
@ -2070,47 +2072,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_export_live.ex #: lib/mv_web/live/import_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_export_live.ex #: lib/mv_web/live/import_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_export_live.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Summary" msgid "Summary"
msgstr "Zusammenfassung" msgstr "Zusammenfassung"
#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Warnings" msgid "Warnings"
msgstr "Warnungen" msgstr "Warnungen"
@ -2202,11 +2204,13 @@ msgstr "Gruppe erfolgreich gespeichert."
#: lib/mv_web/live/components/member_filter_component.ex #: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Groups" msgid "Groups"
msgstr "Gruppen" msgstr "Gruppen"
#: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No groups" msgid "No groups"
msgstr "Keine Gruppen" msgstr "Keine Gruppen"
@ -2325,7 +2329,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_export_live/components.ex #: lib/mv_web/live/import_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"
@ -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." 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_export_live.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_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."
@ -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." 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_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import aborted" msgid "Import aborted"
msgstr "Import abgebrochen" msgstr "Import abgebrochen"
@ -2474,6 +2457,7 @@ msgid "unpaid"
msgstr "Unbezahlt" msgstr "Unbezahlt"
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Member of group %{name}" msgid "Member of group %{name}"
msgstr "Mitglied der Gruppe %{name}" msgstr "Mitglied der Gruppe %{name}"
@ -2614,11 +2598,23 @@ msgstr "Anzahl Mitglieder:"
msgid "PDF" msgid "PDF"
msgstr "PDF" msgstr "PDF"
#~ #: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/components/layouts/sidebar.ex
#~ #, elixir-autogen, elixir-format, fuzzy #: lib/mv_web/live/import_live.ex
#~ msgid "Custom Fields in CSV Import" #, elixir-autogen, elixir-format, fuzzy
#~ msgstr "Benutzerdefinierte Felder" 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 #~ #, elixir-autogen, elixir-format
#~ msgid "Failed to prepare CSV import: %{error}" #~ msgid "Export functionality will be available in a future release."
#~ msgstr "Das Vorbereiten des CSV Imports ist gescheitert: %{error}" #~ 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 "" 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
@ -1543,6 +1544,7 @@ 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"
@ -1860,7 +1862,7 @@ msgstr ""
msgid "updated" msgid "updated"
msgstr "" 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 #: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unknown error" msgid "Unknown error"
@ -1981,37 +1983,37 @@ msgstr ""
msgid "Reset" msgid "Reset"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid " (Field: %{field})" msgid " (Field: %{field})"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "CSV File" msgid "CSV File"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "English Template" msgid "English Template"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Errors" msgid "Errors"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live.ex #: lib/mv_web/live/import_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 ""
@ -2026,27 +2028,27 @@ msgstr ""
msgid "Failed to read file: %{reason}" msgid "Failed to read file: %{reason}"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "German Template" msgid "German Template"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import Results" msgid "Import Results"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live.ex #: lib/mv_web/live/import_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 ""
@ -2061,7 +2063,7 @@ msgstr ""
msgid "Invalid chunk index: %{idx}" msgid "Invalid chunk index: %{idx}"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Line %{line}: %{message}" msgid "Line %{line}: %{message}"
msgstr "" msgstr ""
@ -2071,47 +2073,47 @@ msgstr ""
msgid "No file was uploaded" msgid "No file was uploaded"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live.ex #: lib/mv_web/live/import_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_export_live.ex #: lib/mv_web/live/import_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_export_live.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Start Import" msgid "Start Import"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Starting import..." msgid "Starting import..."
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Summary" msgid "Summary"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Warnings" msgid "Warnings"
msgstr "" msgstr ""
@ -2203,11 +2205,13 @@ msgstr ""
#: lib/mv_web/live/components/member_filter_component.ex #: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Groups" msgid "Groups"
msgstr "" msgstr ""
#: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No groups" msgid "No groups"
msgstr "" msgstr ""
@ -2326,7 +2330,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_export_live/components.ex #: lib/mv_web/live/import_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 ""
@ -2356,43 +2360,22 @@ 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_export_live.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_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 ""
@ -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." 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_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import aborted" msgid "Import aborted"
msgstr "" msgstr ""
@ -2475,6 +2458,7 @@ msgid "unpaid"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Member of group %{name}" msgid "Member of group %{name}"
msgstr "" msgstr ""
@ -2614,3 +2598,9 @@ 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,6 +145,7 @@ 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
@ -1543,6 +1544,7 @@ 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"
@ -1860,7 +1862,7 @@ msgstr ""
msgid "updated" msgid "updated"
msgstr "" 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 #: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unknown error" msgid "Unknown error"
@ -1981,37 +1983,37 @@ msgstr ""
msgid "Reset" msgid "Reset"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid " (Field: %{field})" msgid " (Field: %{field})"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "CSV File" msgid "CSV File"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "English Template" msgid "English Template"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Errors" msgid "Errors"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live.ex #: lib/mv_web/live/import_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 ""
@ -2026,27 +2028,27 @@ msgstr ""
msgid "Failed to read file: %{reason}" msgid "Failed to read file: %{reason}"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "German Template" msgid "German Template"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import Results" msgid "Import Results"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live.ex #: lib/mv_web/live/import_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 ""
@ -2061,7 +2063,7 @@ msgstr ""
msgid "Invalid chunk index: %{idx}" msgid "Invalid chunk index: %{idx}"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Line %{line}: %{message}" msgid "Line %{line}: %{message}"
msgstr "" msgstr ""
@ -2071,47 +2073,47 @@ msgstr ""
msgid "No file was uploaded" msgid "No file was uploaded"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live.ex #: lib/mv_web/live/import_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_export_live.ex #: lib/mv_web/live/import_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_export_live.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Start Import" msgid "Start Import"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Starting import..." msgid "Starting import..."
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Summary" msgid "Summary"
msgstr "" msgstr ""
#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Warnings" msgid "Warnings"
msgstr "" msgstr ""
@ -2203,11 +2205,13 @@ msgstr ""
#: lib/mv_web/live/components/member_filter_component.ex #: lib/mv_web/live/components/member_filter_component.ex
#: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Groups" msgid "Groups"
msgstr "" msgstr ""
#: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/index.ex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No groups" msgid "No groups"
msgstr "" msgstr ""
@ -2326,7 +2330,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_export_live/components.ex #: lib/mv_web/live/import_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 ""
@ -2356,43 +2360,22 @@ 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_export_live.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_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_export_live/components.ex #: lib/mv_web/live/import_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 ""
@ -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." 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_export_live/components.ex #: lib/mv_web/live/import_live/components.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import aborted" msgid "Import aborted"
msgstr "" msgstr ""
@ -2475,6 +2458,7 @@ msgid "unpaid"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Member of group %{name}" msgid "Member of group %{name}"
msgstr "" msgstr ""
@ -2615,7 +2599,23 @@ msgstr "Member count:"
msgid "PDF" msgid "PDF"
msgstr "" 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 #~ #, 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 "" #~ 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-export") {:ok, view, _html} = live(conn, ~p"/admin/import")
# 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.ImportExportLiveTest do defmodule MvWeb.ImportLiveTest do
@moduledoc """ @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 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.ImportExportLiveTest do
# ---------- Business logic: Authorization ---------- # ---------- Business logic: Authorization ----------
describe "Authorization" do 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 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.ImportExportLiveTest 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-export") live(conn, ~p"/admin/import")
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.ImportExportLiveTest 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-export") {:ok, view, _html} = live(conn, ~p"/admin/import")
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.ImportExportLiveTest 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-export") {:ok, view, _html} = live(conn, ~p"/admin/import")
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.ImportExportLiveTest do
conn: conn, conn: conn,
invalid_csv: csv_content invalid_csv: csv_content
} do } 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") 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.ImportExportLiveTest 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-export") {:ok, view, _html} = live(conn, ~p"/admin/import")
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.ImportExportLiveTest 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-export") {:ok, view, _html} = live(conn, ~p"/admin/import")
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.ImportExportLiveTest 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-export") {:ok, view, _html} = live(conn, ~p"/admin/import")
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.ImportExportLiveTest 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-export") {:ok, view, _html} = live(conn, ~p"/admin/import")
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.ImportExportLiveTest 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-export") {:ok, view, _html} = live(conn, ~p"/admin/import")
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.ImportExportLiveTest do
end end
# ---------- UI (smoke / framework): tagged for exclusion from fast CI ---------- # ---------- UI (smoke / framework): tagged for exclusion from fast CI ----------
describe "Import/Export page UI" do describe "Import 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,19 +233,17 @@ defmodule MvWeb.ImportExportLiveTest do
{:ok, conn: conn} {:ok, conn: conn}
end end
test "page loads and shows import form and export placeholder", %{conn: conn} do test "page loads and shows import form", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/import-export") {:ok, view, _html} = live(conn, ~p"/admin/import")
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-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_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']")
@ -258,7 +256,7 @@ defmodule MvWeb.ImportExportLiveTest 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-export") {:ok, view, _html} = live(conn, ~p"/admin/import")
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()
@ -273,7 +271,7 @@ defmodule MvWeb.ImportExportLiveTest 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-export") {:ok, view, _html} = live(conn, ~p"/admin/import")
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 role="status" and aria-label - Badges have aria-label for group membership (no role="status"; reserved for live regions)
- 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 role and aria-label", %{ test "group badges have aria-label for screen readers", %{
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 role="status" and aria-label containing the group name # Verify badges have aria-label containing the group name (no role=status on badges)
assert has_element?(view, "span[role='status'][aria-label*='#{group1.name}']") assert has_element?(view, "span[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

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