feat(import): confirm column mapping in a preview before importing members
This commit is contained in:
parent
a93dd9d535
commit
68a1a9530a
8 changed files with 816 additions and 38 deletions
|
|
@ -75,7 +75,9 @@ defmodule Mv.Membership.Import.HeaderMapper do
|
|||
@ignored_normalized [
|
||||
"membershipfeestatus",
|
||||
"mitgliedsbeitragsstatus",
|
||||
"bezahlstatus"
|
||||
"bezahlstatus",
|
||||
# DE export label for membership_fee_start_date — system-managed, not importable
|
||||
"startdatummitgliedsbeitrag"
|
||||
]
|
||||
|
||||
# Normalized header variants for the groups column. The column is resolved to
|
||||
|
|
|
|||
|
|
@ -99,7 +99,12 @@ defmodule MvWeb.ImportLive do
|
|||
<.form_section title={gettext("Choose CSV file")}>
|
||||
<Components.custom_fields_notice {assigns} />
|
||||
<Components.template_links {assigns} />
|
||||
<Components.import_form {assigns} />
|
||||
<%= if @import_status != :preview do %>
|
||||
<Components.import_form {assigns} />
|
||||
<% end %>
|
||||
<%= if @import_status == :preview do %>
|
||||
<Components.preview {assigns} />
|
||||
<% end %>
|
||||
<%= if @import_status == :running or @import_status == :done or @import_status == :error do %>
|
||||
<Components.import_progress {assigns} />
|
||||
<% end %>
|
||||
|
|
@ -133,6 +138,29 @@ defmodule MvWeb.ImportLive do
|
|||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("confirm_import", _params, socket) do
|
||||
case socket.assigns do
|
||||
%{import_state: import_state} when is_map(import_state) ->
|
||||
start_import(socket, import_state)
|
||||
|
||||
_ ->
|
||||
{:noreply,
|
||||
put_flash(socket, :error, gettext("No prepared import to confirm. Please upload again."))}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("cancel_import", _params, socket) do
|
||||
socket =
|
||||
socket
|
||||
|> assign(:import_state, nil)
|
||||
|> assign(:import_progress, nil)
|
||||
|> assign(:import_status, :idle)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
# Checks if all prerequisites for starting an import are met.
|
||||
#
|
||||
# Validates:
|
||||
|
|
@ -169,10 +197,10 @@ defmodule MvWeb.ImportLive do
|
|||
end
|
||||
end
|
||||
|
||||
# Processes CSV upload and starts import process.
|
||||
# Processes CSV upload and enters the mapping preview.
|
||||
#
|
||||
# Reads the uploaded CSV file, prepares it for import, and initiates
|
||||
# the chunked processing workflow.
|
||||
# Reads the uploaded CSV file and prepares it (read-only at the DB level), then
|
||||
# shows the mapping preview. No member is created until the user confirms.
|
||||
@spec process_csv_upload(Phoenix.LiveView.Socket.t()) ::
|
||||
{:noreply, Phoenix.LiveView.Socket.t()}
|
||||
defp process_csv_upload(socket) do
|
||||
|
|
@ -181,7 +209,7 @@ defmodule MvWeb.ImportLive do
|
|||
with {:ok, content} <- consume_and_read_csv(socket),
|
||||
{:ok, import_state} <-
|
||||
MemberCSV.prepare(content, max_rows: Config.csv_import_max_rows(), actor: actor) do
|
||||
start_import(socket, import_state)
|
||||
enter_preview(socket, import_state)
|
||||
else
|
||||
{:error, reason} when is_binary(reason) ->
|
||||
{:noreply,
|
||||
|
|
@ -193,6 +221,19 @@ defmodule MvWeb.ImportLive do
|
|||
end
|
||||
end
|
||||
|
||||
# Shows the mapping preview without starting any processing.
|
||||
@spec enter_preview(Phoenix.LiveView.Socket.t(), map()) ::
|
||||
{:noreply, Phoenix.LiveView.Socket.t()}
|
||||
defp enter_preview(socket, import_state) do
|
||||
socket =
|
||||
socket
|
||||
|> assign(:import_state, import_state)
|
||||
|> assign(:import_progress, nil)
|
||||
|> assign(:import_status, :preview)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
# Starts the import process by initializing progress tracking and scheduling the first chunk.
|
||||
@spec start_import(Phoenix.LiveView.Socket.t(), map()) ::
|
||||
{:noreply, Phoenix.LiveView.Socket.t()}
|
||||
|
|
@ -263,7 +304,9 @@ defmodule MvWeb.ImportLive do
|
|||
custom_field_lookup: import_state.custom_field_lookup,
|
||||
existing_error_count: length(progress.errors),
|
||||
max_errors: @max_errors,
|
||||
actor: actor
|
||||
actor: actor,
|
||||
fee_type_map: import_state.fee_type_map,
|
||||
groups_found: import_state.groups_found
|
||||
]
|
||||
|
||||
_ =
|
||||
|
|
|
|||
|
|
@ -25,7 +25,22 @@ defmodule MvWeb.ImportLive.Components do
|
|||
<div>
|
||||
<p class="text-sm mb-2">
|
||||
{gettext(
|
||||
"Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, because unknown data field columns will be ignored. Groups and membership fees are not supported for import."
|
||||
"Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, because unknown data field columns will be ignored."
|
||||
)}
|
||||
</p>
|
||||
<p class="text-sm mb-2">
|
||||
{gettext(
|
||||
"Groups column (recognized headers): Groups, Gruppen, Gruppe. Comma-separated group names are supported and missing groups are created automatically."
|
||||
)}
|
||||
</p>
|
||||
<p class="text-sm mb-2">
|
||||
{gettext(
|
||||
"Fee type column (recognized headers): Fee Type, fee type, fee_type, membership_fee_type, Beitragsart. Unknown fee types fall back to the default."
|
||||
)}
|
||||
</p>
|
||||
<p class="text-sm">
|
||||
{gettext(
|
||||
"Fee status columns (Membership Fee Status, Bezahlstatus, Mitgliedsbeitragsstatus) are always ignored and cannot be imported."
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -100,6 +115,194 @@ defmodule MvWeb.ImportLive.Components do
|
|||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders the mapping preview shown between upload and processing.
|
||||
|
||||
Shows the column-to-role mapping, up to 3 sample rows, and notices for
|
||||
auto-created groups, unresolved fee types, empty fee-type cells, and unknown
|
||||
columns. Nothing is written until the user confirms.
|
||||
"""
|
||||
def preview(assigns) do
|
||||
state = assigns.import_state
|
||||
column_roles = column_roles(state)
|
||||
column_samples = column_samples(state.preview_rows, length(state.headers))
|
||||
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(:column_roles, column_roles)
|
||||
|> assign(:column_samples, column_samples)
|
||||
|
||||
~H"""
|
||||
<section
|
||||
class="mt-4 space-y-4"
|
||||
data-testid="import-preview"
|
||||
aria-labelledby="import-preview-heading"
|
||||
>
|
||||
<h2 id="import-preview-heading" class="text-lg font-semibold">
|
||||
{gettext("Preview import")}
|
||||
</h2>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm w-full" data-testid="preview-mapping-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{gettext("Role")}</th>
|
||||
<th>{gettext("Column")}</th>
|
||||
<th class="text-base-content/60">{gettext("Row 1")}</th>
|
||||
<th class="text-base-content/60">{gettext("Row 2")}</th>
|
||||
<th class="text-base-content/60">{gettext("Row 3")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for {{header, role}, samples} <- Enum.zip(@column_roles, @column_samples) do %>
|
||||
<tr class={role_row_class(role)} data-testid="preview-column-row">
|
||||
<td>
|
||||
<span class={"badge badge-sm #{role_badge_class(role)}"}>
|
||||
{role_label(role)}
|
||||
</span>
|
||||
</td>
|
||||
<td class="font-medium">{header}</td>
|
||||
<%= for sample <- samples do %>
|
||||
<td class="text-base-content/70 max-w-32 truncate">{sample}</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<%= if @import_state.groups_to_create != [] do %>
|
||||
<div class="alert alert-info" role="note" data-testid="preview-groups-notice">
|
||||
<.icon name="hero-information-circle" class="size-5" aria-hidden="true" />
|
||||
<div>
|
||||
<p class="text-sm">
|
||||
{gettext("These groups will be created automatically: %{names}",
|
||||
names: Enum.join(@import_state.groups_to_create, ", ")
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= if @import_state.fee_type_warnings != [] do %>
|
||||
<div class="alert alert-warning" role="alert" data-testid="preview-fee-type-warning">
|
||||
<.icon name="hero-exclamation-circle" class="size-5" aria-hidden="true" />
|
||||
<div>
|
||||
<p class="text-sm">
|
||||
{gettext("Unknown fee types (members get the default): %{names}",
|
||||
names: Enum.join(@import_state.fee_type_warnings, ", ")
|
||||
)}
|
||||
</p>
|
||||
<.link
|
||||
navigate={~p"/membership_fee_settings/new_fee_type"}
|
||||
class="link link-primary text-sm"
|
||||
>
|
||||
{gettext("Create fee type")}
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= if @import_state.has_empty_fee_type_cells? do %>
|
||||
<div class="alert alert-info" role="note" data-testid="preview-fee-type-info">
|
||||
<.icon name="hero-information-circle" class="size-5" aria-hidden="true" />
|
||||
<div>
|
||||
<p class="text-sm">
|
||||
{gettext("Rows with an empty fee type will get the default fee type.")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= if @import_state.warnings != [] do %>
|
||||
<div class="alert alert-warning" role="alert" data-testid="preview-unknown-warning">
|
||||
<.icon name="hero-information-circle" class="size-5" aria-hidden="true" />
|
||||
<div>
|
||||
<ul class="list-disc list-inside space-y-1 text-sm">
|
||||
<%= for warning <- @import_state.warnings do %>
|
||||
<li>{warning}</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<.link navigate={~p"/admin/datafields"} class="link link-primary text-sm">
|
||||
{gettext("Create custom field")}
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<.button
|
||||
type="button"
|
||||
phx-click="confirm_import"
|
||||
variant="primary"
|
||||
data-testid="confirm-import-button"
|
||||
>
|
||||
{gettext("Confirm and Import")}
|
||||
</.button>
|
||||
<.button type="button" phx-click="cancel_import" data-testid="cancel-import-button">
|
||||
{gettext("Cancel")}
|
||||
</.button>
|
||||
</div>
|
||||
</section>
|
||||
"""
|
||||
end
|
||||
|
||||
# Pairs each CSV header with its resolved role for the preview mapping table.
|
||||
defp column_roles(state) do
|
||||
member_indices = MapSet.new(Map.values(state.column_map))
|
||||
custom_indices = MapSet.new(Map.values(state.custom_field_map))
|
||||
ignored_headers = MapSet.new(state.ignored)
|
||||
|
||||
state.headers
|
||||
|> Enum.with_index()
|
||||
|> Enum.map(fn {header, index} ->
|
||||
{header, role_for(index, header, state, member_indices, custom_indices, ignored_headers)}
|
||||
end)
|
||||
end
|
||||
|
||||
defp role_for(index, header, state, member_indices, custom_indices, ignored_headers) do
|
||||
cond do
|
||||
index == state.groups_column_index -> :groups
|
||||
index == state.fee_type_column_index -> :fee_type
|
||||
MapSet.member?(ignored_headers, header) -> :ignored
|
||||
MapSet.member?(member_indices, index) -> :member_field
|
||||
MapSet.member?(custom_indices, index) -> :custom_field
|
||||
true -> :unknown
|
||||
end
|
||||
end
|
||||
|
||||
defp role_label(:member_field), do: gettext("Member field")
|
||||
defp role_label(:custom_field), do: gettext("Custom field")
|
||||
defp role_label(:groups), do: gettext("Groups")
|
||||
defp role_label(:fee_type), do: gettext("Fee type")
|
||||
defp role_label(:ignored), do: gettext("Ignored (system-computed field)")
|
||||
defp role_label(:unknown), do: gettext("Unknown (ignored)")
|
||||
|
||||
defp role_badge_class(:member_field), do: "badge-primary"
|
||||
defp role_badge_class(:custom_field), do: "badge-secondary"
|
||||
defp role_badge_class(:groups), do: "badge-success"
|
||||
defp role_badge_class(:fee_type), do: "badge-warning"
|
||||
defp role_badge_class(:ignored), do: "badge-ghost"
|
||||
defp role_badge_class(:unknown), do: "badge-error"
|
||||
|
||||
defp role_row_class(:ignored), do: "opacity-50"
|
||||
defp role_row_class(:unknown), do: "opacity-50"
|
||||
defp role_row_class(_), do: nil
|
||||
|
||||
defp column_samples([], col_count), do: List.duplicate([], col_count)
|
||||
|
||||
defp column_samples(rows, col_count) do
|
||||
Enum.map(0..(col_count - 1), fn col_idx ->
|
||||
rows
|
||||
|> Enum.map(fn row -> Enum.at(row, col_idx, "") end)
|
||||
|> pad_to(3, "")
|
||||
end)
|
||||
end
|
||||
|
||||
defp pad_to(list, target, fill) do
|
||||
list ++ List.duplicate(fill, max(0, target - length(list)))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders import progress text and, when done or aborted, the import results section.
|
||||
"""
|
||||
|
|
@ -246,8 +449,10 @@ defmodule MvWeb.ImportLive.Components do
|
|||
@doc """
|
||||
Returns whether the Start Import button should be disabled.
|
||||
"""
|
||||
@spec import_button_disabled?(:idle | :running | :done | :error, [map()]) :: boolean()
|
||||
@spec import_button_disabled?(:idle | :preview | :running | :done | :error, [map()]) ::
|
||||
boolean()
|
||||
def import_button_disabled?(:running, _entries), do: true
|
||||
def import_button_disabled?(:preview, _entries), do: true
|
||||
def import_button_disabled?(_status, []), do: true
|
||||
def import_button_disabled?(_status, [entry | _]) when not entry.done?, do: true
|
||||
def import_button_disabled?(_status, _entries), do: false
|
||||
|
|
|
|||
|
|
@ -390,6 +390,7 @@ msgstr "Kann jederzeit geändert werden. Änderungen des Betrags betreffen nur z
|
|||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||
#: lib/mv_web/live/group_live/show.ex
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||
#: lib/mv_web/live/member_live/form.ex
|
||||
#: lib/mv_web/live/member_live/show.ex
|
||||
|
|
@ -1329,6 +1330,7 @@ msgstr "Feb."
|
|||
msgid "Fee Type"
|
||||
msgstr "Beitragsart"
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#: lib/mv_web/live/statistics_live.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Fee type"
|
||||
|
|
@ -1488,6 +1490,7 @@ msgstr "Gruppe erfolgreich gespeichert."
|
|||
#: lib/mv_web/components/layouts/sidebar.ex
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#: lib/mv_web/live/group_live/index.ex
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#: lib/mv_web/live/member_live/index.html.heex
|
||||
#: lib/mv_web/live/member_live/show.ex
|
||||
#: lib/mv_web/translations/member_fields.ex
|
||||
|
|
@ -2643,6 +2646,7 @@ msgstr "Geprüft von"
|
|||
msgid "Reviewed at"
|
||||
msgstr "Geprüft am"
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#: lib/mv_web/live/role_live/form.ex
|
||||
#: lib/mv_web/live/user_live/form.ex
|
||||
#: lib/mv_web/live/user_live/index.html.heex
|
||||
|
|
@ -3303,11 +3307,6 @@ msgstr "Aufhebung der Verknüpfung geplant"
|
|||
msgid "Unpaid"
|
||||
msgstr "Unbezahlt"
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, because unknown data field columns will be ignored. Groups and membership fees are not supported for import."
|
||||
msgstr "Verwende die Namen der Datenfelder als Spaltennamen in der CSV-Datei. Datenfelder müssen in Mila bereits angelegt sein, da unbekannte Spaltennamen ignoriert werden. Gruppen und Beitragsstatus können nicht importiert werden."
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "User"
|
||||
|
|
@ -3977,3 +3976,103 @@ msgstr "Beitragsart '%{name}' nicht gefunden; Standard-Beitragsart wird verwende
|
|||
#, elixir-autogen, elixir-format
|
||||
msgid "Group assignment failed: %{reason}"
|
||||
msgstr "Gruppenzuordnung fehlgeschlagen: %{reason}"
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Confirm and Import"
|
||||
msgstr "Bestätigen und importieren"
|
||||
|
||||
#: lib/mv_web/live/import_live.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "No prepared import to confirm. Please upload again."
|
||||
msgstr "Kein vorbereiteter Import zum Bestätigen. Bitte erneut hochladen."
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Preview import"
|
||||
msgstr "Importvorschau"
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Column"
|
||||
msgstr "Spalte"
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Custom field"
|
||||
msgstr "Benutzerdefiniertes Feld"
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Ignored (system-computed field)"
|
||||
msgstr "Ignoriert (vom System berechnetes Feld)"
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Member field"
|
||||
msgstr "Mitgliedsfeld"
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Rows with an empty fee type will get the default fee type."
|
||||
msgstr "Zeilen ohne Beitragsart erhalten die Standard-Beitragsart."
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "These groups will be created automatically: %{names}"
|
||||
msgstr "Diese Gruppen werden automatisch erstellt: %{names}"
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Unknown (ignored)"
|
||||
msgstr "Unbekannt (ignoriert)"
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Unknown fee types (members get the default): %{names}"
|
||||
msgstr "Unbekannte Beitragsarten (Mitglieder erhalten den Standard): %{names}"
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Fee status columns (Membership Fee Status, Bezahlstatus, Mitgliedsbeitragsstatus) are always ignored and cannot be imported."
|
||||
msgstr "Beitragsstatus-Spalten (Membership Fee Status, Bezahlstatus, Mitgliedsbeitragsstatus) werden immer ignoriert und können nicht importiert werden."
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Groups column (recognized headers): Groups, Gruppen, Gruppe. Comma-separated group names are supported and missing groups are created automatically."
|
||||
msgstr "Gruppen-Spalte (erkannte Spaltennamen): Groups, Gruppen, Gruppe. Mehrere durch Komma getrennte Gruppennamen werden unterstützt; fehlende Gruppen werden automatisch erstellt."
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, because unknown data field columns will be ignored."
|
||||
msgstr "Verwende die Namen der Datenfelder als Spaltennamen in der CSV-Datei. Datenfelder müssen in Mila bereits angelegt sein, da unbekannte Spaltennamen ignoriert werden."
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Fee type column (recognized headers): Fee Type, fee type, fee_type, membership_fee_type, Beitragsart. Unknown fee types fall back to the default."
|
||||
msgstr "Beitragsart-Spalte (erkannte Spaltennamen): Fee Type, fee type, fee_type, membership_fee_type, Beitragsart. Unbekannte Beitragsarten erhalten die Standard-Beitragsart."
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Create custom field"
|
||||
msgstr "Datenfeld erstellen"
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Create fee type"
|
||||
msgstr "Beitragsart erstellen"
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Row 1"
|
||||
msgstr "Zeile 1"
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Row 2"
|
||||
msgstr "Zeile 2"
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Row 3"
|
||||
msgstr "Zeile 3"
|
||||
|
|
|
|||
|
|
@ -391,6 +391,7 @@ msgstr ""
|
|||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||
#: lib/mv_web/live/group_live/show.ex
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||
#: lib/mv_web/live/member_live/form.ex
|
||||
#: lib/mv_web/live/member_live/show.ex
|
||||
|
|
@ -1330,6 +1331,7 @@ msgstr ""
|
|||
msgid "Fee Type"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#: lib/mv_web/live/statistics_live.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Fee type"
|
||||
|
|
@ -1489,6 +1491,7 @@ msgstr ""
|
|||
#: lib/mv_web/components/layouts/sidebar.ex
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#: lib/mv_web/live/group_live/index.ex
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#: lib/mv_web/live/member_live/index.html.heex
|
||||
#: lib/mv_web/live/member_live/show.ex
|
||||
#: lib/mv_web/translations/member_fields.ex
|
||||
|
|
@ -2644,6 +2647,7 @@ msgstr ""
|
|||
msgid "Reviewed at"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#: lib/mv_web/live/role_live/form.ex
|
||||
#: lib/mv_web/live/user_live/form.ex
|
||||
#: lib/mv_web/live/user_live/index.html.heex
|
||||
|
|
@ -3304,11 +3308,6 @@ msgstr ""
|
|||
msgid "Unpaid"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, because unknown data field columns will be ignored. Groups and membership fees are not supported for import."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "User"
|
||||
|
|
@ -3977,3 +3976,103 @@ msgstr ""
|
|||
#, elixir-autogen, elixir-format
|
||||
msgid "Group assignment failed: %{reason}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Confirm and Import"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "No prepared import to confirm. Please upload again."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Preview import"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Column"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Custom field"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Ignored (system-computed field)"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Member field"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Rows with an empty fee type will get the default fee type."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "These groups will be created automatically: %{names}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Unknown (ignored)"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Unknown fee types (members get the default): %{names}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Fee status columns (Membership Fee Status, Bezahlstatus, Mitgliedsbeitragsstatus) are always ignored and cannot be imported."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Groups column (recognized headers): Groups, Gruppen, Gruppe. Comma-separated group names are supported and missing groups are created automatically."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, because unknown data field columns will be ignored."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Fee type column (recognized headers): Fee Type, fee type, fee_type, membership_fee_type, Beitragsart. Unknown fee types fall back to the default."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Create custom field"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Create fee type"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Row 1"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Row 2"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Row 3"
|
||||
msgstr ""
|
||||
|
|
|
|||
|
|
@ -391,6 +391,7 @@ msgstr ""
|
|||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||
#: lib/mv_web/live/group_live/show.ex
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||
#: lib/mv_web/live/member_live/form.ex
|
||||
#: lib/mv_web/live/member_live/show.ex
|
||||
|
|
@ -1330,6 +1331,7 @@ msgstr ""
|
|||
msgid "Fee Type"
|
||||
msgstr "Fee Type"
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#: lib/mv_web/live/statistics_live.ex
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Fee type"
|
||||
|
|
@ -1489,6 +1491,7 @@ msgstr ""
|
|||
#: lib/mv_web/components/layouts/sidebar.ex
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#: lib/mv_web/live/group_live/index.ex
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#: lib/mv_web/live/member_live/index.html.heex
|
||||
#: lib/mv_web/live/member_live/show.ex
|
||||
#: lib/mv_web/translations/member_fields.ex
|
||||
|
|
@ -2644,6 +2647,7 @@ msgstr "Review by"
|
|||
msgid "Reviewed at"
|
||||
msgstr "Review date"
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#: lib/mv_web/live/role_live/form.ex
|
||||
#: lib/mv_web/live/user_live/form.ex
|
||||
#: lib/mv_web/live/user_live/index.html.heex
|
||||
|
|
@ -3304,11 +3308,6 @@ msgstr ""
|
|||
msgid "Unpaid"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, because unknown data field columns will be ignored. Groups and membership fees are not supported for import."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "User"
|
||||
|
|
@ -3977,3 +3976,103 @@ msgstr ""
|
|||
#, elixir-autogen, elixir-format
|
||||
msgid "Group assignment failed: %{reason}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Confirm and Import"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "No prepared import to confirm. Please upload again."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Preview import"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Column"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Custom field"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Ignored (system-computed field)"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Member field"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Rows with an empty fee type will get the default fee type."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "These groups will be created automatically: %{names}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Unknown (ignored)"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Unknown fee types (members get the default): %{names}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Fee status columns (Membership Fee Status, Bezahlstatus, Mitgliedsbeitragsstatus) are always ignored and cannot be imported."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Groups column (recognized headers): Groups, Gruppen, Gruppe. Comma-separated group names are supported and missing groups are created automatically."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, because unknown data field columns will be ignored."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Fee type column (recognized headers): Fee Type, fee type, fee_type, membership_fee_type, Beitragsart. Unknown fee types fall back to the default."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Create custom field"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Create fee type"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Row 1"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Row 2"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/import_live/components.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Row 3"
|
||||
msgstr ""
|
||||
|
|
|
|||
31
test/mv_web/live/import_live/components_test.exs
Normal file
31
test/mv_web/live/import_live/components_test.exs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
defmodule MvWeb.ImportLive.ComponentsTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias MvWeb.ImportLive.Components
|
||||
|
||||
describe "import_button_disabled?/2" do
|
||||
@done_entry %{done?: true}
|
||||
|
||||
test "disables the Start Import button while the preview is displayed" do
|
||||
# During :preview the upload entry is done, but re-clicking Start Import
|
||||
# would re-run the upload processing and overwrite the current preview.
|
||||
assert Components.import_button_disabled?(:preview, [@done_entry]) == true
|
||||
end
|
||||
|
||||
test "disables the button while an import is running" do
|
||||
assert Components.import_button_disabled?(:running, [@done_entry]) == true
|
||||
end
|
||||
|
||||
test "disables the button when there are no upload entries" do
|
||||
assert Components.import_button_disabled?(:idle, []) == true
|
||||
end
|
||||
|
||||
test "disables the button while an upload entry is not yet done" do
|
||||
assert Components.import_button_disabled?(:idle, [%{done?: false}]) == true
|
||||
end
|
||||
|
||||
test "enables the button at idle with a completed upload" do
|
||||
assert Components.import_button_disabled?(:idle, [@done_entry]) == false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -27,6 +27,16 @@ defmodule MvWeb.ImportLiveTest do
|
|||
|
||||
defp submit_import(view), do: view |> form("#csv-upload-form", %{}) |> render_submit()
|
||||
|
||||
defp confirm_import(view),
|
||||
do: view |> element("[data-testid='confirm-import-button']") |> render_click()
|
||||
|
||||
# Full flow: upload, enter preview (start), then confirm to begin processing.
|
||||
defp run_full_import(view, csv_content, filename \\ "test_import.csv") do
|
||||
upload_csv_file(view, csv_content, filename)
|
||||
submit_import(view)
|
||||
confirm_import(view)
|
||||
end
|
||||
|
||||
defp wait_for_import_completion, do: Process.sleep(1000)
|
||||
|
||||
# ---------- Business logic: Authorization ----------
|
||||
|
|
@ -56,8 +66,7 @@ defmodule MvWeb.ImportLiveTest do
|
|||
|> File.read!()
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
||||
upload_csv_file(view, csv_content)
|
||||
submit_import(view)
|
||||
run_full_import(view, csv_content)
|
||||
wait_for_import_completion()
|
||||
|
||||
assert has_element?(view, "[data-testid='import-results-panel']")
|
||||
|
|
@ -121,8 +130,7 @@ defmodule MvWeb.ImportLiveTest do
|
|||
invalid_csv: csv_content
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
||||
upload_csv_file(view, csv_content, "invalid_import.csv")
|
||||
submit_import(view)
|
||||
run_full_import(view, csv_content, "invalid_import.csv")
|
||||
wait_for_import_completion()
|
||||
|
||||
assert has_element?(view, "[data-testid='import-results-panel']")
|
||||
|
|
@ -141,8 +149,7 @@ defmodule MvWeb.ImportLiveTest do
|
|||
invalid_rows =
|
||||
for i <- 1..100, do: "Row#{i};Last#{i};;Country#{i};City#{i};Street#{i};12345\n"
|
||||
|
||||
upload_csv_file(view, header <> Enum.join(invalid_rows), "large_invalid.csv")
|
||||
submit_import(view)
|
||||
run_full_import(view, header <> Enum.join(invalid_rows), "large_invalid.csv")
|
||||
wait_for_import_completion()
|
||||
|
||||
assert has_element?(view, "[data-testid='import-results-panel']")
|
||||
|
|
@ -174,8 +181,7 @@ defmodule MvWeb.ImportLiveTest do
|
|||
Path.join([__DIR__, "..", "..", "fixtures", "csv_with_bom_semicolon.csv"])
|
||||
|> File.read!()
|
||||
|
||||
upload_csv_file(view, csv_content, "bom_import.csv")
|
||||
submit_import(view)
|
||||
run_full_import(view, csv_content, "bom_import.csv")
|
||||
wait_for_import_completion()
|
||||
|
||||
assert has_element?(view, "[data-testid='import-results-panel']")
|
||||
|
|
@ -193,8 +199,7 @@ defmodule MvWeb.ImportLiveTest do
|
|||
Path.join([__DIR__, "..", "..", "fixtures", "csv_with_empty_lines.csv"])
|
||||
|> File.read!()
|
||||
|
||||
upload_csv_file(view, csv_content, "empty_lines.csv")
|
||||
submit_import(view)
|
||||
run_full_import(view, csv_content, "empty_lines.csv")
|
||||
wait_for_import_completion()
|
||||
|
||||
assert has_element?(view, "[data-testid='import-error-list']")
|
||||
|
|
@ -208,8 +213,7 @@ defmodule MvWeb.ImportLiveTest do
|
|||
unknown_custom_field_csv: csv_content
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
||||
upload_csv_file(view, csv_content, "unknown_custom.csv")
|
||||
submit_import(view)
|
||||
run_full_import(view, csv_content, "unknown_custom.csv")
|
||||
wait_for_import_completion()
|
||||
|
||||
assert has_element?(view, "[data-testid='import-results-panel']")
|
||||
|
|
@ -254,14 +258,27 @@ defmodule MvWeb.ImportLiveTest do
|
|||
assert has_element?(view, "[data-testid='csv-upload-form'] input[type='file']")
|
||||
end
|
||||
|
||||
test "custom fields notice lists accepted groups and fee-type column names", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/import")
|
||||
|
||||
# Groups column variants (both EN and DE)
|
||||
assert html =~ "Groups"
|
||||
assert html =~ "Gruppen"
|
||||
# Fee type column variants (both EN and DE)
|
||||
assert html =~ "Beitragsart"
|
||||
assert html =~ "Fee Type"
|
||||
assert html =~ "fee type"
|
||||
# Fee status is always ignored (named explicitly)
|
||||
assert html =~ "Bezahlstatus"
|
||||
end
|
||||
|
||||
test "after successful import, progress container has aria-live", %{conn: conn} do
|
||||
csv_content =
|
||||
Path.join([__DIR__, "..", "..", "fixtures", "valid_member_import.csv"])
|
||||
|> File.read!()
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
||||
upload_csv_file(view, csv_content)
|
||||
submit_import(view)
|
||||
run_full_import(view, csv_content)
|
||||
wait_for_import_completion()
|
||||
assert has_element?(view, "[data-testid='import-progress-container']")
|
||||
html = render(view)
|
||||
|
|
@ -280,4 +297,187 @@ defmodule MvWeb.ImportLiveTest do
|
|||
html = render(view)
|
||||
assert html =~ "Failed to prepare"
|
||||
end
|
||||
|
||||
describe "preview state machine" do
|
||||
setup %{conn: conn} do
|
||||
admin_user = Mv.Fixtures.user_with_role_fixture("admin")
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> MvWeb.ConnCase.conn_with_password_user(admin_user)
|
||||
|> put_locale_en()
|
||||
|
||||
valid_csv =
|
||||
Path.join([__DIR__, "..", "..", "fixtures", "valid_member_import.csv"])
|
||||
|> File.read!()
|
||||
|
||||
{:ok, conn: conn, valid_csv: valid_csv}
|
||||
end
|
||||
|
||||
test "start_import transitions to preview without processing", %{
|
||||
conn: conn,
|
||||
valid_csv: csv_content
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
||||
upload_csv_file(view, csv_content)
|
||||
submit_import(view)
|
||||
|
||||
# Preview is shown; no results panel yet because nothing was processed.
|
||||
assert has_element?(view, "[data-testid='import-preview']")
|
||||
refute has_element?(view, "[data-testid='import-results-panel']")
|
||||
|
||||
# No member was created during preview (read-only step).
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
{:ok, members} = Membership.list_members(actor: system_actor)
|
||||
|
||||
refute Enum.any?(
|
||||
members,
|
||||
&(&1.email in ["alice.smith@example.com", "bob.johnson@example.com"])
|
||||
)
|
||||
end
|
||||
|
||||
test "confirm_import starts processing and creates members", %{
|
||||
conn: conn,
|
||||
valid_csv: csv_content
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
||||
run_full_import(view, csv_content)
|
||||
wait_for_import_completion()
|
||||
|
||||
assert has_element?(view, "[data-testid='import-results-panel']")
|
||||
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
{:ok, members} = Membership.list_members(actor: system_actor)
|
||||
|
||||
imported =
|
||||
Enum.filter(
|
||||
members,
|
||||
&(&1.email in ["alice.smith@example.com", "bob.johnson@example.com"])
|
||||
)
|
||||
|
||||
assert length(imported) == 2
|
||||
end
|
||||
|
||||
test "cancel_import returns to idle and hides the preview", %{
|
||||
conn: conn,
|
||||
valid_csv: csv_content
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
||||
upload_csv_file(view, csv_content)
|
||||
submit_import(view)
|
||||
assert has_element?(view, "[data-testid='import-preview']")
|
||||
|
||||
view |> element("[data-testid='cancel-import-button']") |> render_click()
|
||||
|
||||
refute has_element?(view, "[data-testid='import-preview']")
|
||||
refute has_element?(view, "[data-testid='import-results-panel']")
|
||||
end
|
||||
end
|
||||
|
||||
describe "preview contents" do
|
||||
setup %{conn: conn} do
|
||||
admin_user = Mv.Fixtures.user_with_role_fixture("admin")
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> MvWeb.ConnCase.conn_with_password_user(admin_user)
|
||||
|> put_locale_en()
|
||||
|
||||
{:ok, conn: conn}
|
||||
end
|
||||
|
||||
test "shows the column mapping table with roles for each column", %{conn: conn} do
|
||||
csv = "email;Gruppen;Beitragsart;Bezahlstatus;UnknownCol\na@e.com;Chor;Premium;paid;x"
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
||||
upload_csv_file(view, csv)
|
||||
submit_import(view)
|
||||
|
||||
assert has_element?(view, "[data-testid='preview-mapping-table']")
|
||||
html = render(view)
|
||||
|
||||
assert html =~ "email"
|
||||
assert html =~ "Gruppen"
|
||||
assert html =~ "Beitragsart"
|
||||
assert html =~ "Bezahlstatus"
|
||||
assert html =~ "UnknownCol"
|
||||
end
|
||||
|
||||
test "lists every CSV column exactly once in the mapping table", %{conn: conn} do
|
||||
headers = ["email", "Gruppen", "Beitragsart", "Bezahlstatus", "UnknownCol"]
|
||||
csv = Enum.join(headers, ";") <> "\na@e.com;Chor;Premium;paid;x"
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
||||
upload_csv_file(view, csv)
|
||||
submit_import(view)
|
||||
|
||||
# Count the data rows via their stable testid so the assertion is independent
|
||||
# of how Phoenix renders class attributes or tr tags (§1.15).
|
||||
html = render(view)
|
||||
|
||||
row_count =
|
||||
html |> String.split(~s(data-testid="preview-column-row")) |> length() |> Kernel.-(1)
|
||||
|
||||
assert row_count == length(headers)
|
||||
end
|
||||
|
||||
test "shows up to 3 sample data rows", %{conn: conn} do
|
||||
csv = "email\nr1@e.com\nr2@e.com\nr3@e.com\nr4@e.com"
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
||||
upload_csv_file(view, csv)
|
||||
submit_import(view)
|
||||
|
||||
html = render(view)
|
||||
assert html =~ "r1@e.com"
|
||||
assert html =~ "r2@e.com"
|
||||
assert html =~ "r3@e.com"
|
||||
refute html =~ "r4@e.com"
|
||||
end
|
||||
|
||||
test "shows an auto-create notice for unknown group names", %{conn: conn} do
|
||||
csv = "email;Gruppen\na@e.com;Ganz Neue Gruppe"
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
||||
upload_csv_file(view, csv)
|
||||
submit_import(view)
|
||||
|
||||
assert has_element?(view, "[data-testid='preview-groups-notice']")
|
||||
assert render(view) =~ "Ganz Neue Gruppe"
|
||||
end
|
||||
|
||||
test "shows a warning and link for unknown fee-type names", %{conn: conn} do
|
||||
csv = "email;Beitragsart\na@e.com;Phantom Tarif"
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
||||
upload_csv_file(view, csv)
|
||||
submit_import(view)
|
||||
|
||||
assert has_element?(view, "[data-testid='preview-fee-type-warning']")
|
||||
html = render(view)
|
||||
assert html =~ "Phantom Tarif"
|
||||
assert html =~ "/membership_fee_settings"
|
||||
end
|
||||
|
||||
test "shows an info notice when fee-type cells are empty", %{conn: conn} do
|
||||
csv = "email;Beitragsart\na@e.com;\nb@e.com;"
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
||||
upload_csv_file(view, csv)
|
||||
submit_import(view)
|
||||
|
||||
assert has_element?(view, "[data-testid='preview-fee-type-info']")
|
||||
end
|
||||
|
||||
test "shows a warning for unknown custom-field columns", %{conn: conn} do
|
||||
csv = "email;TotallyUnknown\na@e.com;value"
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
||||
upload_csv_file(view, csv)
|
||||
submit_import(view)
|
||||
|
||||
assert has_element?(view, "[data-testid='preview-unknown-warning']")
|
||||
assert render(view) =~ "TotallyUnknown"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue