Refinex CSV import and PDf export closes #299 and #433 #446

Merged
carla merged 16 commits from feat/299_plz into main 2026-02-24 16:32:32 +01:00
2 changed files with 76 additions and 9 deletions
Showing only changes of commit 2408978180 - Show all commits

View file

@ -17,15 +17,24 @@ defmodule Mv.Membership.Import.HeaderMapper do
## Member Field Mapping ## Member Field Mapping
Maps CSV headers to canonical member fields: Maps CSV headers to canonical member fields (same as `Mv.Constants.member_fields()` for
- `email` (required) importable attributes). All DB-backed member attributes can be imported.
- `first_name` (optional)
- `last_name` (optional)
- `street` (optional)
- `postal_code` (optional)
- `city` (optional)
Supports both English and German variants (e.g., "Email" / "E-Mail", "First Name" / "Vorname"). - `email` (required)
- `first_name`, `last_name` (optional)
- `join_date`, `exit_date` (optional, ISO-8601 date)
- `notes` (optional)
- `country`, `city`, `street`, `house_number`, `postal_code` (optional)
- `membership_fee_start_date` (optional, ISO-8601 date)
Supports English and German header variants (e.g. "Email" / "E-Mail", "Join Date" / "Beitrittsdatum").
## Fields not supported for import
- **membership_fee_status** Computed (calculation from membership fee cycles). Not stored;
cannot be set via CSV. Export can include it.
- **groups** Many-to-many relationship (through member_groups). Import would require
resolving group names/slugs to IDs and creating associations; not in current import scope.
## Custom Field Detection ## Custom Field Detection
@ -75,11 +84,37 @@ defmodule Mv.Membership.Import.HeaderMapper do
"nachname", "nachname",
"familienname" "familienname"
], ],
join_date: [
"join date",
"join_date",
"beitrittsdatum",
"beitritts-datum"
],
exit_date: [
"exit date",
"exit_date",
"austrittsdatum",
"austritts-datum"
],
notes: [
"notes",
"notizen",
"bemerkungen"
],
street: [ street: [
"street", "street",
"address", "address",
"strasse" "strasse"
], ],
house_number: [
"house number",
"house_number",
"house no",
"hausnummer",
"nr",
"nr.",
"nummer"
],
postal_code: [ postal_code: [
"postal code", "postal code",
"postal_code", "postal_code",
@ -93,6 +128,18 @@ defmodule Mv.Membership.Import.HeaderMapper do
"town", "town",
"stadt", "stadt",
"ort" "ort"
],
country: [
"country",
"land",
"staat"
],
membership_fee_start_date: [
"membership fee start date",
"membership_fee_start_date",
"fee start",
"beitragsbeginn",
"beitrags-beginn"
] ]
} }

View file

@ -549,9 +549,12 @@ defmodule Mv.Membership.Import.MemberCSV do
line_number, line_number,
actor actor
) do ) do
# Convert empty strings to nil for date fields so Ash accepts them
member_attrs = sanitize_date_fields(trimmed_member_attrs)
# Create member with custom field values # Create member with custom field values
member_attrs_with_cf = member_attrs_with_cf =
trimmed_member_attrs member_attrs
|> Map.put(:custom_field_values, custom_field_values) |> Map.put(:custom_field_values, custom_field_values)
# Only include custom_field_values if not empty # Only include custom_field_values if not empty
@ -793,6 +796,23 @@ defmodule Mv.Membership.Import.MemberCSV do
end) end)
end end
# Converts empty strings to nil for date fields so Ash can accept them
@date_fields [:join_date, :exit_date, :membership_fee_start_date]
defp sanitize_date_fields(attrs) when is_map(attrs) do
Enum.reduce(@date_fields, attrs, fn field, acc ->
put_date_field(acc, field, Map.get(acc, field))
end)
end
defp put_date_field(acc, field, ""), do: Map.put(acc, field, nil)
defp put_date_field(acc, field, val) when is_binary(val) do
if String.trim(val) == "", do: Map.put(acc, field, nil), else: acc
end
defp put_date_field(acc, _field, _), do: acc
# Formats Ash errors into MemberCSV.Error structs # Formats Ash errors into MemberCSV.Error structs
defp format_ash_error(%Ash.Error.Invalid{errors: errors}, line_number, email) do defp format_ash_error(%Ash.Error.Invalid{errors: errors}, line_number, email) do
# Try to find email-related errors first (for better error messages) # Try to find email-related errors first (for better error messages)