From eea3f28cc52ad1eeddf9aa594f927e5fa97d7f2e Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 15 Dec 2025 09:33:47 +0100 Subject: [PATCH 01/23] test: added tests --- .../mv_web/live/global_settings_live_test.exs | 16 ++ .../index_component_test.exs | 190 ++++++++++++++++++ .../index_required_display_test.exs | 157 +++++++++++++++ 3 files changed, 363 insertions(+) create mode 100644 test/mv_web/live/member_field_live/index_component_test.exs create mode 100644 test/mv_web/member_live/index_required_display_test.exs diff --git a/test/mv_web/live/global_settings_live_test.exs b/test/mv_web/live/global_settings_live_test.exs index 6a739b5..86680f3 100644 --- a/test/mv_web/live/global_settings_live_test.exs +++ b/test/mv_web/live/global_settings_live_test.exs @@ -64,5 +64,21 @@ defmodule MvWeb.GlobalSettingsLiveTest do assert html =~ "must be present" end + + test "displays Memberdata section", %{conn: conn} do + {:ok, _view, html} = live(conn, ~p"/settings") + + assert html =~ "Memberdata" or html =~ "Member Data" + end + + test "displays flash message after member field visibility update", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/settings") + + # Simulate member field visibility update + send(view.pid, {:member_field_visibility_updated}) + + # Check for flash message + assert render(view) =~ "updated" or render(view) =~ "success" + end end end diff --git a/test/mv_web/live/member_field_live/index_component_test.exs b/test/mv_web/live/member_field_live/index_component_test.exs new file mode 100644 index 0000000..88c1a5b --- /dev/null +++ b/test/mv_web/live/member_field_live/index_component_test.exs @@ -0,0 +1,190 @@ +defmodule MvWeb.MemberFieldLive.IndexComponentTest do + @moduledoc """ + Tests for MemberFieldLive.IndexComponent. + + Tests cover: + - Rendering all member fields from Mv.Constants.member_fields() + - Displaying show_in_overview status as badge (Yes/No) + - Displaying required status for required fields (first_name, last_name, email) + - Toggle functionality to change show_in_overview flag + - Settings are correctly updated after toggle + - Current status is displayed based on settings.member_field_visibility + - Default status is "Yes" (visible) when not configured in settings + """ + use MvWeb.ConnCase, async: false + + import Phoenix.LiveViewTest + + alias Mv.Membership + + setup %{conn: conn} do + user = create_test_user(%{email: "admin@example.com"}) + conn = conn_with_oidc_user(conn, user) + {:ok, conn: conn, user: user} + end + + describe "rendering" do + test "renders all member fields from Constants", %{conn: conn} do + {:ok, view, html} = live(conn, ~p"/settings") + + # Check that all member fields are displayed + member_fields = Mv.Constants.member_fields() + + for field <- member_fields do + field_name = String.replace(Atom.to_string(field), "_", " ") |> String.capitalize() + # Field name should appear in the table (either as label or in some form) + assert html =~ field_name or html =~ Atom.to_string(field) + end + end + + test "displays show_in_overview status as badge", %{conn: conn} do + {:ok, view, html} = live(conn, ~p"/settings") + + # Should have "Show in overview" column header + assert html =~ "Show in overview" or html =~ "Show in Overview" + + # Should have badge elements (Yes/No) + assert html =~ "badge" or html =~ "Yes" or html =~ "No" + end + + test "displays required status for required fields", %{conn: conn} do + {:ok, _view, html} = live(conn, ~p"/settings") + + # Required fields: first_name, last_name, email + # Should have "Required" column or indicator + assert html =~ "Required" or html =~ "required" + end + + test "shows default status as Yes when not configured", %{conn: conn} do + # Ensure settings have no member_field_visibility configured + {:ok, settings} = Membership.get_settings() + + {:ok, _updated} = + Membership.update_settings(settings, %{member_field_visibility: %{}}) + + {:ok, _view, html} = live(conn, ~p"/settings") + + # All fields should show as visible (Yes) by default + # Check for "Yes" badge or similar indicator + assert html =~ "Yes" or html =~ "badge-success" + end + + test "shows configured visibility status from settings", %{conn: conn} do + # Configure some fields as hidden + {:ok, settings} = Membership.get_settings() + visibility_config = %{"street" => false, "house_number" => false} + + {:ok, _updated} = + Membership.update_member_field_visibility(settings, visibility_config) + + {:ok, _view, html} = live(conn, ~p"/settings") + + # Street and house_number should show as hidden (No) + # Other fields should show as visible (Yes) + assert html =~ "street" or html =~ "Street" + assert html =~ "house_number" or html =~ "House number" + end + end + + describe "toggle functionality" do + test "toggles field visibility from visible to hidden", %{conn: conn} do + # Start with field visible (default) + {:ok, settings} = Membership.get_settings() + + {:ok, _updated} = + Membership.update_member_field_visibility(settings, %{"street" => true}) + + {:ok, view, _html} = live(conn, ~p"/settings") + + # Find and click toggle button for street field + # This will fail until component is implemented + assert has_element?(view, "#member-field-street-toggle") or + has_element?(view, "[phx-click='toggle_field_visibility'][data-field='street']") + + # Click toggle + view + |> element("#member-field-street-toggle") + |> render_click(%{"field" => "street"}) + + # Verify settings updated + {:ok, updated_settings} = Membership.get_settings() + visibility = updated_settings.member_field_visibility || %{} + assert Map.get(visibility, "street") == false + end + + test "toggles field visibility from hidden to visible", %{conn: conn} do + # Start with field hidden + {:ok, settings} = Membership.get_settings() + + {:ok, _updated} = + Membership.update_member_field_visibility(settings, %{"street" => false}) + + {:ok, view, _html} = live(conn, ~p"/settings") + + # Click toggle to make visible + view + |> element("#member-field-street-toggle") + |> render_click(%{"field" => "street"}) + + # Verify settings updated + {:ok, updated_settings} = Membership.get_settings() + visibility = updated_settings.member_field_visibility || %{} + assert Map.get(visibility, "street") == true + end + + test "sends message to parent LiveView after toggle", %{conn: conn} do + {:ok, settings} = Membership.get_settings() + + {:ok, _updated} = + Membership.update_member_field_visibility(settings, %{"street" => true}) + + {:ok, view, _html} = live(conn, ~p"/settings") + + # Toggle field + view + |> element("#member-field-street-toggle") + |> render_click(%{"field" => "street"}) + + # Check for flash message (handled by parent LiveView) + assert render(view) =~ "updated" or render(view) =~ "success" + end + end + + describe "required fields" do + test "marks first_name as required", %{conn: conn} do + {:ok, _view, html} = live(conn, ~p"/settings") + + # first_name should be marked as required + assert html =~ "first_name" or html =~ "First name" + # Should have required indicator + assert html =~ "required" or html =~ "Required" + end + + test "marks last_name as required", %{conn: conn} do + {:ok, _view, html} = live(conn, ~p"/settings") + + # last_name should be marked as required + assert html =~ "last_name" or html =~ "Last name" + # Should have required indicator + assert html =~ "required" or html =~ "Required" + end + + test "marks email as required", %{conn: conn} do + {:ok, _view, html} = live(conn, ~p"/settings") + + # email should be marked as required + assert html =~ "email" or html =~ "Email" + # Should have required indicator + assert html =~ "required" or html =~ "Required" + end + + test "does not mark optional fields as required", %{conn: conn} do + {:ok, _view, html} = live(conn, ~p"/settings") + + # Optional fields should not have required indicator + # Check that street (optional) doesn't have required badge + # This test verifies that only required fields show the indicator + assert html =~ "street" or html =~ "Street" + end + end +end diff --git a/test/mv_web/member_live/index_required_display_test.exs b/test/mv_web/member_live/index_required_display_test.exs new file mode 100644 index 0000000..eb61fea --- /dev/null +++ b/test/mv_web/member_live/index_required_display_test.exs @@ -0,0 +1,157 @@ +defmodule MvWeb.MemberLive.IndexRequiredDisplayTest do + @moduledoc """ + Tests for displaying "required" badge in member overview. + + Tests cover: + - "required" badge for required member fields (first_name, last_name, email) + - "required" badge for required custom fields + - No "required" badge for optional member fields + - No "required" badge for optional custom fields + - Badge is positioned in column header + """ + # async: false to prevent PostgreSQL deadlocks when creating members and custom fields + use MvWeb.ConnCase, async: false + import Phoenix.LiveViewTest + require Ash.Query + + alias Mv.Membership.{CustomField, CustomFieldValue, Member} + + setup do + # Create test member + {:ok, member} = + Member + |> Ash.Changeset.for_create(:create_member, %{ + first_name: "Alice", + last_name: "Anderson", + email: "alice@example.com" + }) + |> Ash.create() + + # Create required custom field + {:ok, required_field} = + CustomField + |> Ash.Changeset.for_create(:create, %{ + name: "emergency_contact", + value_type: :string, + required: true, + show_in_overview: true + }) + |> Ash.create() + + # Create optional custom field + {:ok, optional_field} = + CustomField + |> Ash.Changeset.for_create(:create, %{ + name: "hobby", + value_type: :string, + required: false, + show_in_overview: true + }) + |> Ash.create() + + # Create custom field values + {:ok, _cfv1} = + CustomFieldValue + |> Ash.Changeset.for_create(:create, %{ + member_id: member.id, + custom_field_id: required_field.id, + value: %{"_union_type" => "string", "_union_value" => "John Doe"} + }) + |> Ash.create() + + {:ok, _cfv2} = + CustomFieldValue + |> Ash.Changeset.for_create(:create, %{ + member_id: member.id, + custom_field_id: optional_field.id, + value: %{"_union_type" => "string", "_union_value" => "Reading"} + }) + |> Ash.create() + + %{ + member: member, + required_field: required_field, + optional_field: optional_field + } + end + + describe "required badge for member fields" do + test "displays required badge for first_name column", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/members") + + # Check that first_name column header has required badge + assert html =~ "first_name" or html =~ "First name" or html =~ "First Name" + # Should have required indicator in header + assert html =~ "required" or html =~ "Required" + end + + test "displays required badge for last_name column", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/members") + + # Check that last_name column header has required badge + assert html =~ "last_name" or html =~ "Last name" or html =~ "Last Name" + # Should have required indicator in header + assert html =~ "required" or html =~ "Required" + end + + test "displays required badge for email column", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/members") + + # Check that email column header has required badge + assert html =~ "email" or html =~ "Email" + # Should have required indicator in header + assert html =~ "required" or html =~ "Required" + end + + test "does not display required badge for optional member fields", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/members") + + # Optional fields: street, city, phone_number, etc. + # These should not have required badge + # We check that street is present but doesn't have required indicator nearby + assert html =~ "street" or html =~ "Street" + end + end + + describe "required badge for custom fields" do + test "displays required badge for required custom field column", %{ + conn: conn, + required_field: field + } do + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/members") + + # Check that required custom field column header has required badge + assert html =~ field.name + # Should have required indicator in header + assert html =~ "required" or html =~ "Required" + end + + test "does not display required badge for optional custom field column", %{ + conn: conn, + optional_field: field + } do + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/members") + + # Check that optional custom field column header does not have required badge + assert html =~ field.name + # Should not have required indicator (or it should be clear it's optional) + end + end + + describe "badge positioning" do + test "required badge is in column header, not in cell content", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, "/members") + + # Required badge should be in thead (header), not in tbody (data rows) + # This is verified by checking that required appears near column headers + assert html =~ "thead" or html =~ "th" + end + end +end From 756d99dcc823bdc247dd7d79499678497732292e Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 15 Dec 2025 09:54:52 +0100 Subject: [PATCH 02/23] test: adds tests --- test/mv_web/live/member_field_live/index_component_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/mv_web/live/member_field_live/index_component_test.exs b/test/mv_web/live/member_field_live/index_component_test.exs index 88c1a5b..e2e1be3 100644 --- a/test/mv_web/live/member_field_live/index_component_test.exs +++ b/test/mv_web/live/member_field_live/index_component_test.exs @@ -25,7 +25,7 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do describe "rendering" do test "renders all member fields from Constants", %{conn: conn} do - {:ok, view, html} = live(conn, ~p"/settings") + {:ok, _view, html} = live(conn, ~p"/settings") # Check that all member fields are displayed member_fields = Mv.Constants.member_fields() @@ -38,7 +38,7 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do end test "displays show_in_overview status as badge", %{conn: conn} do - {:ok, view, html} = live(conn, ~p"/settings") + {:ok, _view, html} = live(conn, ~p"/settings") # Should have "Show in overview" column header assert html =~ "Show in overview" or html =~ "Show in Overview" From 3d81461fbeeb8ef8bb40357adb744bc0f9a4f998 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 15 Dec 2025 09:58:19 +0100 Subject: [PATCH 03/23] feat: adds memberdata component for settings --- lib/mv_web/live/global_settings_live.ex | 33 +++ .../live/member_field_live/index_component.ex | 206 ++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 lib/mv_web/live/member_field_live/index_component.ex diff --git a/lib/mv_web/live/global_settings_live.ex b/lib/mv_web/live/global_settings_live.ex index 0b3ec1c..87a1a4d 100644 --- a/lib/mv_web/live/global_settings_live.ex +++ b/lib/mv_web/live/global_settings_live.ex @@ -62,6 +62,12 @@ defmodule MvWeb.GlobalSettingsLive do + <%!-- Memberdata Section --%> + <.live_component + module={MvWeb.MemberFieldLive.IndexComponent} + id="member-fields-component" + settings={@settings} + /> <%!-- Custom Fields Section --%> <.live_component module={MvWeb.CustomFieldLive.IndexComponent} @@ -125,6 +131,33 @@ defmodule MvWeb.GlobalSettingsLive do {:noreply, put_flash(socket, :error, gettext("Slug does not match. Deletion cancelled."))} end + @impl true + def handle_info({:member_field_visibility_updated}, socket) do + # Reload settings to get updated member_field_visibility + {:ok, updated_settings} = Membership.get_settings() + + {:noreply, + socket + |> assign(:settings, updated_settings) + |> put_flash(:info, gettext("Member field visibility updated successfully"))} + end + + @impl true + def handle_info({:member_field_visibility_error, error}, socket) do + error_message = + case error do + %Ash.Error.Invalid{} = invalid_error -> + gettext("Failed to update member field visibility: %{error}", + error: Ash.ErrorKind.message(invalid_error) + ) + + error -> + gettext("Failed to update member field visibility: %{error}", error: inspect(error)) + end + + {:noreply, put_flash(socket, :error, error_message)} + end + defp assign_form(%{assigns: %{settings: settings}} = socket) do form = AshPhoenix.Form.for_update( diff --git a/lib/mv_web/live/member_field_live/index_component.ex b/lib/mv_web/live/member_field_live/index_component.ex new file mode 100644 index 0000000..faa62b5 --- /dev/null +++ b/lib/mv_web/live/member_field_live/index_component.ex @@ -0,0 +1,206 @@ +defmodule MvWeb.MemberFieldLive.IndexComponent do + @moduledoc """ + LiveComponent for managing member field visibility in overview (embedded in settings). + + ## Features + - List all member fields from Mv.Constants.member_fields() + - Display show_in_overview status as badge (Yes/No) + - Display required status for required fields (first_name, last_name, email) + - Toggle show_in_overview flag for each field + - Updates Settings.member_field_visibility + """ + use MvWeb, :live_component + + alias Mv.Membership + + @required_fields [:first_name, :last_name, :email] + + @impl true + def render(assigns) do + assigns = + assigns + |> assign(:member_fields, get_member_fields_with_visibility(assigns.settings)) + |> assign(:required?, &required?/1) + + ~H""" +
+ <.form_section title={gettext("Memberdata")}> +

+ {gettext("These fields are neccessary for MILA to handle member identification and payment calculations in the future. This you cannot delete these fields but hide them in the member overview.")} +

+ + <.table id="member_fields" rows={@member_fields}> + <:col :let={{_field_name, field_data}} label={gettext("Field Name")}> + {format_field_name(field_data.field)} + + + <:col + :let={{_field_name, field_data}} + label={gettext("Required")} + class="max-w-[9.375rem] text-center" + > + + {gettext("Required")} + + + {gettext("Optional")} + + + + <:col + :let={{_field_name, field_data}} + label={gettext("Show in overview")} + class="max-w-[9.375rem] text-center" + > + + {gettext("Yes")} + + + {gettext("No")} + + + + <:action :let={{_field_name, field_data}}> + + + + +
+ """ + end + + @impl true + def update(assigns, socket) do + {:ok, + socket + |> assign(assigns) + |> assign_new(:settings, fn -> get_settings() end)} + end + + @impl true + def handle_event("toggle_field_visibility", %{"field" => field_string}, socket) do + # Validate that the field is a valid member field before converting to atom + valid_fields = Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1) + + if field_string in valid_fields do + {:ok, settings} = Membership.get_settings() + + # Get current visibility config + current_visibility = settings.member_field_visibility || %{} + # Normalize keys to strings + normalized_visibility = + Enum.reduce(current_visibility, %{}, fn + {key, value}, acc when is_atom(key) -> + Map.put(acc, Atom.to_string(key), value) + + {key, value}, acc when is_binary(key) -> + Map.put(acc, key, value) + end) + + # Toggle the field visibility + current_value = Map.get(normalized_visibility, field_string, true) + new_value = !current_value + updated_visibility = Map.put(normalized_visibility, field_string, new_value) + + # Update settings + case Membership.update_member_field_visibility(settings, updated_visibility) do + {:ok, updated_settings} -> + # Send message to parent LiveView + send(self(), {:member_field_visibility_updated}) + + {:noreply, + socket + |> assign(:settings, updated_settings)} + + {:error, error} -> + # Send error message to parent LiveView for user feedback + send(self(), {:member_field_visibility_error, error}) + + {:noreply, socket} + end + else + {:noreply, socket} + end + end + + # Helper functions + + defp get_settings do + case Membership.get_settings() do + {:ok, settings} -> + settings + + {:error, _} -> + # Return a minimal struct-like map for fallback + # This is only used for initial rendering, actual settings will be loaded properly + %{member_field_visibility: %{}} + end + end + + defp get_member_fields_with_visibility(settings) do + member_fields = Mv.Constants.member_fields() + visibility_config = settings.member_field_visibility || %{} + + # Normalize visibility config keys to atoms + normalized_config = normalize_visibility_config(visibility_config) + + Enum.map(member_fields, fn field -> + show_in_overview = Map.get(normalized_config, field, true) + + {Atom.to_string(field), %{field: field, show_in_overview: show_in_overview}} + end) + end + + defp normalize_visibility_config(config) when is_map(config) do + Enum.reduce(config, %{}, fn + {key, value}, acc when is_atom(key) -> + Map.put(acc, key, value) + + {key, value}, acc when is_binary(key) -> + try do + atom_key = String.to_existing_atom(key) + Map.put(acc, atom_key, value) + rescue + ArgumentError -> + acc + end + + _, acc -> + acc + end) + end + + defp normalize_visibility_config(_), do: %{} + + defp required?(field) when field in @required_fields, do: true + defp required?(_), do: false + + defp format_field_name(field) when is_atom(field) do + field + |> Atom.to_string() + |> String.replace("_", " ") + |> String.split() + |> Enum.map_join(" ", &String.capitalize/1) + end +end From e088123fb9ffd65fe6f8699676b99a8a4c79a4cb Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 15 Dec 2025 09:58:38 +0100 Subject: [PATCH 04/23] feat: adds required column to custom field settings --- .../live/custom_field_live/index_component.ex | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/mv_web/live/custom_field_live/index_component.ex b/lib/mv_web/live/custom_field_live/index_component.ex index 8f63bf8..ca67799 100644 --- a/lib/mv_web/live/custom_field_live/index_component.ex +++ b/lib/mv_web/live/custom_field_live/index_component.ex @@ -68,6 +68,19 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do {custom_field.description} + <:col + :let={{_id, custom_field}} + label={gettext("Required")} + class="max-w-[9.375rem] text-center" + > + + {gettext("Required")} + + + {gettext("Optional")} + + + <:col :let={{_id, custom_field}} label={gettext("Show in overview")} From 18c082a8936cac16fa0cf44a09a944b89ee3052b Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 15 Dec 2025 10:50:36 +0100 Subject: [PATCH 05/23] chore: updated translation --- .../live/member_field_live/index_component.ex | 4 +- priv/gettext/de/LC_MESSAGES/default.po | 52 +++++++++++++++++++ priv/gettext/default.pot | 52 +++++++++++++++++++ priv/gettext/en/LC_MESSAGES/default.po | 52 +++++++++++++++++++ 4 files changed, 159 insertions(+), 1 deletion(-) diff --git a/lib/mv_web/live/member_field_live/index_component.ex b/lib/mv_web/live/member_field_live/index_component.ex index faa62b5..61c6578 100644 --- a/lib/mv_web/live/member_field_live/index_component.ex +++ b/lib/mv_web/live/member_field_live/index_component.ex @@ -26,7 +26,9 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do
<.form_section title={gettext("Memberdata")}>

- {gettext("These fields are neccessary for MILA to handle member identification and payment calculations in the future. This you cannot delete these fields but hide them in the member overview.")} + {gettext( + "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview." + )}

<.table id="member_fields" rows={@member_fields}> diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 25f685d..088304b 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -97,6 +97,7 @@ msgstr "Nachname" msgid "New Member" msgstr "Neues Mitglied" +#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/user_live/index.html.heex #, elixir-autogen, elixir-format @@ -183,6 +184,7 @@ msgid "Street" msgstr "Straße" #: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index/formatter.ex #: lib/mv_web/live/member_live/show.ex @@ -196,6 +198,7 @@ msgid "Show Member" msgstr "Mitglied anzeigen" #: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index/formatter.ex #: lib/mv_web/live/member_live/show.ex @@ -345,6 +348,8 @@ msgid "Profil" msgstr "Profil" #: lib/mv_web/live/custom_field_live/form_component.ex +#: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Required" msgstr "Erforderlich" @@ -668,6 +673,7 @@ msgstr "Um die Löschung zu bestätigen, gib bitte folgenden Text ein:" #: lib/mv_web/live/custom_field_live/form_component.ex #: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Show in overview" msgstr "In Übersicht anzeigen" @@ -1438,6 +1444,52 @@ msgstr "Textfeld" msgid "Yes/No-Selection" msgstr "Ja/Nein-Auswahl" +#: lib/mv_web/live/global_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Failed to update member field visibility: %{error}" +msgstr "Fehler beim anpassen der Sichtbarkeit des Feldes: %{error}" + +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "Field Name" +msgstr "Name des Datenfelds" + +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "Hide" +msgstr "Ausblenden" + +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "Hide %{field} in overview" +msgstr "Verstecke %{field} in der Übersicht" + +#: lib/mv_web/live/global_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Member field visibility updated successfully" +msgstr "Sichtbarkeit des Feldes erfolgreich aktualisiert." + +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Memberdata" +msgstr "Mitgliederdaten" + +#: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "Optional" +msgstr "Optional" + +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "Show %{field} in overview" +msgstr "" + +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview." +msgstr "Diese Datenfelder sind für MILA notwendig um Mitglieder zu identifizieren und zukünftig Beitragszahlungen zu berechnen. Aus diesem Grund können sie nicht gelöscht, aber in der Übersicht ausgeblendet werden." + #~ #: lib/mv_web/live/custom_field_live/show.ex #~ #, elixir-autogen, elixir-format #~ msgid "Auto-generated identifier (immutable)" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index a7ab36b..0677858 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -98,6 +98,7 @@ msgstr "" msgid "New Member" msgstr "" +#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/user_live/index.html.heex #, elixir-autogen, elixir-format @@ -184,6 +185,7 @@ msgid "Street" msgstr "" #: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index/formatter.ex #: lib/mv_web/live/member_live/show.ex @@ -197,6 +199,7 @@ msgid "Show Member" msgstr "" #: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index/formatter.ex #: lib/mv_web/live/member_live/show.ex @@ -346,6 +349,8 @@ msgid "Profil" 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/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Required" msgstr "" @@ -669,6 +674,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/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Show in overview" msgstr "" @@ -1438,3 +1444,49 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Yes/No-Selection" msgstr "" + +#: lib/mv_web/live/global_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Failed to update member field visibility: %{error}" +msgstr "" + +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "Field Name" +msgstr "" + +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "Hide" +msgstr "" + +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "Hide %{field} in overview" +msgstr "" + +#: lib/mv_web/live/global_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Member field visibility updated successfully" +msgstr "" + +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "Memberdata" +msgstr "" + +#: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "Optional" +msgstr "" + +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "Show %{field} in overview" +msgstr "" + +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview." +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index e2a1876..131f4dc 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -98,6 +98,7 @@ msgstr "" msgid "New Member" msgstr "" +#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/user_live/index.html.heex #, elixir-autogen, elixir-format @@ -184,6 +185,7 @@ msgid "Street" msgstr "" #: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index/formatter.ex #: lib/mv_web/live/member_live/show.ex @@ -197,6 +199,7 @@ msgid "Show Member" msgstr "" #: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index/formatter.ex #: lib/mv_web/live/member_live/show.ex @@ -346,6 +349,8 @@ msgid "Profil" 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/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Required" msgstr "" @@ -669,6 +674,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/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Show in overview" msgstr "" @@ -1439,6 +1445,52 @@ msgstr "" msgid "Yes/No-Selection" msgstr "" +#: lib/mv_web/live/global_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Failed to update member field visibility: %{error}" +msgstr "" + +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "Field Name" +msgstr "" + +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "Hide" +msgstr "" + +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "Hide %{field} in overview" +msgstr "" + +#: lib/mv_web/live/global_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Member field visibility updated successfully" +msgstr "" + +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Memberdata" +msgstr "" + +#: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Optional" +msgstr "" + +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "Show %{field} in overview" +msgstr "" + +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview." +msgstr "" + #~ #: lib/mv_web/live/custom_field_live/show.ex #~ #, elixir-autogen, elixir-format #~ msgid "Auto-generated identifier (immutable)" From 5fa0b48acc9e658f5357693beeb4209f0727bdbf Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 16 Dec 2025 17:12:26 +0100 Subject: [PATCH 06/23] feat: adds form for member fields --- .../live/member_field_live/form_component.ex | 445 ++++++++++++++++++ .../live/member_field_live/index_component.ex | 252 +++++----- 2 files changed, 592 insertions(+), 105 deletions(-) create mode 100644 lib/mv_web/live/member_field_live/form_component.ex diff --git a/lib/mv_web/live/member_field_live/form_component.ex b/lib/mv_web/live/member_field_live/form_component.ex new file mode 100644 index 0000000..a9985cb --- /dev/null +++ b/lib/mv_web/live/member_field_live/form_component.ex @@ -0,0 +1,445 @@ +defmodule MvWeb.MemberFieldLive.FormComponent do + @moduledoc """ + LiveComponent form for editing member field properties (embedded in settings). + + ## Features + - Edit member field properties (name, value type, description, immutable, required, show in overview) + - Display member field information from Member Resource + - Restrict editing for email field (only show_in_overview can be changed) + - Real-time validation + - Updates Settings.member_field_visibility + + ## Props + - `member_field` - The member field atom to edit (e.g., :first_name, :email) + - `settings` - The current Settings resource + - `on_save` - Callback function to call when form is saved + - `on_cancel` - Callback function to call when form is cancelled + """ + use MvWeb, :live_component + + alias Mv.Membership + alias MvWeb.Translations.MemberFields + alias MvWeb.Translations.FieldTypes + + @required_fields [:first_name, :last_name, :email] + + @impl true + def render(assigns) do + assigns = + assigns + |> assign(:field_attributes, get_field_attributes(assigns.member_field)) + |> assign(:is_email_field?, assigns.member_field == :email) + |> assign(:field_label, MemberFields.label(assigns.member_field)) + + ~H""" +
+
+
+ <.button + type="button" + phx-click="cancel" + phx-target={@myself} + aria-label={gettext("Back to member field overview")} + > + <.icon name="hero-arrow-left" class="w-4 h-4" /> + +

+ {gettext("Edit Field: %{field}", field: @field_label)} +

+
+ + <.form + for={@form} + id={@id <> "-form"} + phx-change="validate" + phx-submit="save" + phx-target={@myself} + > +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ <.input + :if={not @is_email_field?} + field={@form[:description]} + type="text" + label={gettext("Description")} + disabled={@is_email_field?} + readonly={@is_email_field?} + /> + +
+
+ +
+
+ <.input + :if={not @is_email_field?} + field={@form[:immutable]} + type="checkbox" + label={gettext("Immutable")} + disabled={@is_email_field?} + readonly={@is_email_field?} + /> + +
+
+ +
+
+ <.input + :if={not @is_email_field?} + field={@form[:required]} + type="checkbox" + label={gettext("Required")} + disabled={@is_email_field?} + readonly={@is_email_field?} + /> + + <.input + field={@form[:show_in_overview]} + type="checkbox" + label={gettext("Show in overview")} + /> + +
+ <.button type="button" phx-click="cancel" phx-target={@myself}> + {gettext("Cancel")} + + <.button phx-disable-with={gettext("Saving...")} variant="primary"> + {gettext("Save Field")} + +
+ +
+
+ """ + end + + @impl true + def update(assigns, socket) do + {:ok, + socket + |> assign(assigns) + |> assign_form()} + end + + @impl true + def handle_event("validate", %{"member_field" => member_field_params}, socket) do + # For member fields, we only validate show_in_overview + # Other fields are read-only or derived from the Member Resource + form = socket.assigns.form + + updated_params = + member_field_params + |> Map.put("show_in_overview", parse_boolean(member_field_params["show_in_overview"])) + |> Map.put("name", form.source["name"]) + |> Map.put("value_type", form.source["value_type"]) + |> Map.put("description", form.source["description"]) + |> Map.put("immutable", form.source["immutable"]) + |> Map.put("required", form.source["required"]) + + updated_form = + form + |> Map.put(:value, updated_params) + |> Map.put(:errors, []) + + {:noreply, assign(socket, form: updated_form)} + end + + @impl true + def handle_event("save", %{"member_field" => member_field_params}, socket) do + # Only show_in_overview can be changed for member fields + show_in_overview = parse_boolean(member_field_params["show_in_overview"]) + + # Get current visibility config and update only the current field + current_visibility = socket.assigns.settings.member_field_visibility || %{} + field_string = Atom.to_string(socket.assigns.member_field) + + # Normalize keys to strings + normalized_visibility = + Enum.reduce(current_visibility, %{}, fn + {key, value}, acc when is_atom(key) -> + Map.put(acc, Atom.to_string(key), value) + + {key, value}, acc when is_binary(key) -> + Map.put(acc, key, value) + end) + + # Update the specific field + updated_visibility = Map.put(normalized_visibility, field_string, show_in_overview) + + # Update settings with new visibility + case Membership.update_member_field_visibility( + socket.assigns.settings, + updated_visibility + ) do + {:ok, _updated_settings} -> + socket.assigns.on_save.(socket.assigns.member_field, "update") + {:noreply, socket} + + {:error, error} -> + # Add error to form + form = + socket.assigns.form + |> Map.put(:errors, [ + %{field: :show_in_overview, message: format_error(error)} + ]) + + {:noreply, assign(socket, form: form)} + end + end + + @impl true + def handle_event("cancel", _params, socket) do + socket.assigns.on_cancel.() + {:noreply, socket} + end + + # Helper functions + + defp assign_form(%{assigns: %{member_field: member_field, settings: settings}} = socket) do + field_attributes = get_field_attributes(member_field) + visibility_config = settings.member_field_visibility || %{} + normalized_config = normalize_visibility_config(visibility_config) + show_in_overview = Map.get(normalized_config, member_field, true) + + # Create a manual form structure with string keys + form_data = %{ + "name" => MemberFields.label(member_field), + "value_type" => format_value_type(field_attributes.value_type), + "description" => field_attributes.description || "", + "immutable" => field_attributes.immutable, + "required" => field_attributes.required, + "show_in_overview" => show_in_overview + } + + form = to_form(form_data, as: "member_field") + + assign(socket, form: form) + end + + defp get_field_attributes(field) when is_atom(field) do + # Get attribute info from Member Resource + case Ash.Resource.Info.attribute(Mv.Membership.Member, field) do + nil -> + # Fallback for fields not in resource (shouldn't happen with Constants) + %{ + value_type: :string, + description: nil, + immutable: field == :email, + required: field in @required_fields + } + + attribute -> + %{ + value_type: attribute.type, + description: nil, + immutable: field == :email, + required: not attribute.allow_nil? + } + end + end + + defp format_value_type(type) when is_atom(type) do + type_string = to_string(type) + + # Check if it's an Ash type module (e.g., Ash.Type.String or Elixir.Ash.Type.String) + if String.contains?(type_string, "Ash.Type.") do + # Extract the base type name from Ash type modules + # e.g., "Elixir.Ash.Type.String" -> "String" -> :string + type_name = + type_string + |> String.split(".") + |> List.last() + |> String.downcase() + + try do + type_atom = String.to_existing_atom(type_name) + FieldTypes.label(type_atom) + rescue + ArgumentError -> + # Fallback if atom doesn't exist + FieldTypes.label(:string) + end + else + # It's already an atom like :string, :boolean, :date + FieldTypes.label(type) + end + end + + defp format_value_type(type) do + # Fallback for unknown types + to_string(type) + end + + defp normalize_visibility_config(config) when is_map(config) do + Enum.reduce(config, %{}, fn + {key, value}, acc when is_atom(key) -> + Map.put(acc, key, value) + + {key, value}, acc when is_binary(key) -> + try do + atom_key = String.to_existing_atom(key) + Map.put(acc, atom_key, value) + rescue + ArgumentError -> + acc + end + + _, acc -> + acc + end) + end + + defp normalize_visibility_config(_), do: %{} + + defp parse_boolean(value) when is_boolean(value), do: value + defp parse_boolean("true"), do: true + defp parse_boolean("false"), do: false + defp parse_boolean(1), do: true + defp parse_boolean(0), do: false + defp parse_boolean(_), do: false + + defp format_error(%Ash.Error.Invalid{} = error) do + Ash.ErrorKind.message(error) + end + + defp format_error(error) do + inspect(error) + end +end diff --git a/lib/mv_web/live/member_field_live/index_component.ex b/lib/mv_web/live/member_field_live/index_component.ex index 61c6578..7422f5a 100644 --- a/lib/mv_web/live/member_field_live/index_component.ex +++ b/lib/mv_web/live/member_field_live/index_component.ex @@ -6,12 +6,14 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do - List all member fields from Mv.Constants.member_fields() - Display show_in_overview status as badge (Yes/No) - Display required status for required fields (first_name, last_name, email) - - Toggle show_in_overview flag for each field + - Edit member field properties (expandable form like custom fields) - Updates Settings.member_field_visibility """ use MvWeb, :live_component alias Mv.Membership + alias MvWeb.Translations.MemberFields + alias MvWeb.Translations.FieldTypes @required_fields [:first_name, :last_name, :email] @@ -24,123 +26,123 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do ~H"""
- <.form_section title={gettext("Memberdata")}> -

- {gettext( - "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview." - )} -

+

+ {gettext( + "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview." + )} +

- <.table id="member_fields" rows={@member_fields}> - <:col :let={{_field_name, field_data}} label={gettext("Field Name")}> - {format_field_name(field_data.field)} - + <%!-- Show form when editing --%> +
+ <.live_component + module={MvWeb.MemberFieldLive.FormComponent} + id={@form_id} + member_field={@editing_member_field} + settings={@settings} + on_save={ + fn member_field, action -> + send(self(), {:member_field_saved, member_field, action}) + end + } + on_cancel={fn -> send_update(__MODULE__, id: @id, show_form: false) end} + /> +
- <:col - :let={{_field_name, field_data}} - label={gettext("Required")} - class="max-w-[9.375rem] text-center" + <%!-- Hide table when form is visible --%> + <.table + :if={!@show_form} + id="member_fields" + rows={@member_fields} + > + <:col :let={{_field_name, field_data}} label={gettext("Name")}> + {MemberFields.label(field_data.field)} + + + <:col :let={{_field_name, field_data}} label={gettext("Value Type")}> + {format_value_type(field_data.field)} + + + <:col :let={{_field_name, field_data}} label={gettext("Description")}> + {field_data.description || ""} + + + <:col + :let={{_field_name, field_data}} + label={gettext("Required")} + class="max-w-[9.375rem] text-center" + > + - - {gettext("Required")} - - - {gettext("Optional")} - - + {gettext("Required")} + + + {gettext("Optional")} + + - <:col - :let={{_field_name, field_data}} - label={gettext("Show in overview")} - class="max-w-[9.375rem] text-center" + <:col + :let={{_field_name, field_data}} + label={gettext("Show in overview")} + class="max-w-[9.375rem] text-center" + > + + {gettext("Yes")} + + + {gettext("No")} + + + + <:action :let={{_field_name, field_data}}> + <.link + phx-click="edit_member_field" + phx-value-field={Atom.to_string(field_data.field)} + phx-target={@myself} > - - {gettext("Yes")} - - - {gettext("No")} - - - - <:action :let={{_field_name, field_data}}> - - - - + {gettext("Edit")} + + +
""" end @impl true def update(assigns, socket) do + # If show_form is explicitly provided in assigns, reset editing state + socket = + if Map.has_key?(assigns, :show_form) and assigns.show_form == false do + socket + |> assign(:editing_member_field, nil) + |> assign(:form_id, "member-field-form-new") + else + socket + end + {:ok, socket |> assign(assigns) - |> assign_new(:settings, fn -> get_settings() end)} + |> assign_new(:settings, fn -> get_settings() end) + |> assign_new(:show_form, fn -> false end) + |> assign_new(:form_id, fn -> "member-field-form-new" end) + |> assign_new(:editing_member_field, fn -> nil end)} end @impl true - def handle_event("toggle_field_visibility", %{"field" => field_string}, socket) do + def handle_event("edit_member_field", %{"field" => field_string}, socket) do # Validate that the field is a valid member field before converting to atom valid_fields = Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1) if field_string in valid_fields do - {:ok, settings} = Membership.get_settings() + field_atom = String.to_existing_atom(field_string) - # Get current visibility config - current_visibility = settings.member_field_visibility || %{} - # Normalize keys to strings - normalized_visibility = - Enum.reduce(current_visibility, %{}, fn - {key, value}, acc when is_atom(key) -> - Map.put(acc, Atom.to_string(key), value) - - {key, value}, acc when is_binary(key) -> - Map.put(acc, key, value) - end) - - # Toggle the field visibility - current_value = Map.get(normalized_visibility, field_string, true) - new_value = !current_value - updated_visibility = Map.put(normalized_visibility, field_string, new_value) - - # Update settings - case Membership.update_member_field_visibility(settings, updated_visibility) do - {:ok, updated_settings} -> - # Send message to parent LiveView - send(self(), {:member_field_visibility_updated}) - - {:noreply, - socket - |> assign(:settings, updated_settings)} - - {:error, error} -> - # Send error message to parent LiveView for user feedback - send(self(), {:member_field_visibility_error, error}) - - {:noreply, socket} - end + {:noreply, + socket + |> assign(:show_form, true) + |> assign(:editing_member_field, field_atom) + |> assign(:form_id, "member-field-form-#{field_string}")} else {:noreply, socket} end @@ -169,9 +171,57 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do Enum.map(member_fields, fn field -> show_in_overview = Map.get(normalized_config, field, true) + attribute = Ash.Resource.Info.attribute(Mv.Membership.Member, field) - {Atom.to_string(field), %{field: field, show_in_overview: show_in_overview}} + %{ + field: field, + show_in_overview: show_in_overview, + value_type: (attribute && attribute.type) || :string, + description: nil + } end) + |> Enum.map(fn field_data -> + {Atom.to_string(field_data.field), field_data} + end) + end + + defp format_value_type(field) when is_atom(field) do + case Ash.Resource.Info.attribute(Mv.Membership.Member, field) do + nil -> FieldTypes.label(:string) + attribute -> format_value_type(attribute.type) + end + end + + defp format_value_type(type) when is_atom(type) do + type_string = to_string(type) + + # Check if it's an Ash type module (e.g., Ash.Type.String or Elixir.Ash.Type.String) + if String.contains?(type_string, "Ash.Type.") do + # Extract the base type name from Ash type modules + # e.g., "Elixir.Ash.Type.String" -> "String" -> :string + type_name = + type_string + |> String.split(".") + |> List.last() + |> String.downcase() + + try do + type_atom = String.to_existing_atom(type_name) + FieldTypes.label(type_atom) + rescue + ArgumentError -> + # Fallback if atom doesn't exist + FieldTypes.label(:string) + end + else + # It's already an atom like :string, :boolean, :date + FieldTypes.label(type) + end + end + + defp format_value_type(type) do + # Fallback for unknown types + to_string(type) end defp normalize_visibility_config(config) when is_map(config) do @@ -197,12 +247,4 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do defp required?(field) when field in @required_fields, do: true defp required?(_), do: false - - defp format_field_name(field) when is_atom(field) do - field - |> Atom.to_string() - |> String.replace("_", " ") - |> String.split() - |> Enum.map_join(" ", &String.capitalize/1) - end end From c88f805b6ebdaab646b2e485481deda3c7e8f5fa Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 16 Dec 2025 17:16:29 +0100 Subject: [PATCH 07/23] style: combines member and custom fields in settings --- .../live/custom_field_live/index_component.ex | 286 +++++++++--------- lib/mv_web/live/global_settings_live.ex | 40 ++- 2 files changed, 171 insertions(+), 155 deletions(-) diff --git a/lib/mv_web/live/custom_field_live/index_component.ex b/lib/mv_web/live/custom_field_live/index_component.ex index ca67799..5de2ebf 100644 --- a/lib/mv_web/live/custom_field_live/index_component.ex +++ b/lib/mv_web/live/custom_field_live/index_component.ex @@ -17,165 +17,161 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do assigns = assign(assigns, :field_type_label, &MvWeb.Translations.FieldTypes.label/1) ~H""" -
- <.form_section title={gettext("Custom Fields")}> -
-

- {gettext("These will appear in addition to other data when adding new members.")} -

-
- <.button - class="ml-auto" - variant="primary" - phx-click="new_custom_field" - phx-target={@myself} - > - <.icon name="hero-plus" /> {gettext("New Custom field")} - -
+
+
+

+ {gettext("These will appear in addition to other data when adding new members.")} +

+
+ <.button + class="ml-auto" + variant="primary" + phx-click="new_custom_field" + phx-target={@myself} + > + <.icon name="hero-plus" /> {gettext("New Custom field")} +
- <%!-- Show form when creating or editing --%> -
- <.live_component - module={MvWeb.CustomFieldLive.FormComponent} - id={@form_id} - custom_field={@editing_custom_field} - on_save={ - fn custom_field, action -> send(self(), {:custom_field_saved, custom_field, action}) end - } - on_cancel={fn -> send_update(__MODULE__, id: @id, show_form: false) end} - /> -
- - <%!-- Hide table when form is visible --%> - <.table - :if={!@show_form} - id="custom_fields" - rows={@streams.custom_fields} - row_click={ - fn {_id, custom_field} -> - JS.push("edit_custom_field", value: %{id: custom_field.id}, target: @myself) - end +
+ <%!-- Show form when creating or editing --%> +
+ <.live_component + module={MvWeb.CustomFieldLive.FormComponent} + id={@form_id} + custom_field={@editing_custom_field} + on_save={ + fn custom_field, action -> send(self(), {:custom_field_saved, custom_field, action}) end } + on_cancel={fn -> send_update(__MODULE__, id: @id, show_form: false) end} + /> +
+ + <%!-- Hide table when form is visible --%> + <.table + :if={!@show_form} + id="custom_fields" + rows={@streams.custom_fields} + row_click={ + fn {_id, custom_field} -> + JS.push("edit_custom_field", value: %{id: custom_field.id}, target: @myself) + end + } + > + <:col :let={{_id, custom_field}} label={gettext("Name")}>{custom_field.name} + + <:col :let={{_id, custom_field}} label={gettext("Value Type")}> + {@field_type_label.(custom_field.value_type)} + + + <:col :let={{_id, custom_field}} label={gettext("Description")}> + {custom_field.description} + + + <:col + :let={{_id, custom_field}} + label={gettext("Required")} + class="max-w-[9.375rem] text-center" > - <:col :let={{_id, custom_field}} label={gettext("Name")}>{custom_field.name} + + {gettext("Required")} + + + {gettext("Optional")} + + - <:col :let={{_id, custom_field}} label={gettext("Value Type")}> - {@field_type_label.(custom_field.value_type)} - + <:col + :let={{_id, custom_field}} + label={gettext("Show in overview")} + class="max-w-[9.375rem] text-center" + > + + {gettext("Yes")} + + + {gettext("No")} + + - <:col :let={{_id, custom_field}} label={gettext("Description")}> - {custom_field.description} - + <:action :let={{_id, custom_field}}> + <.link phx-click={ + JS.push("edit_custom_field", value: %{id: custom_field.id}, target: @myself) + }> + {gettext("Edit")} + + - <:col - :let={{_id, custom_field}} - label={gettext("Required")} - class="max-w-[9.375rem] text-center" - > - - {gettext("Required")} - - - {gettext("Optional")} - - + <:action :let={{_id, custom_field}}> + <.link phx-click={JS.push("prepare_delete", value: %{id: custom_field.id}, target: @myself)}> + {gettext("Delete")} + + + - <:col - :let={{_id, custom_field}} - label={gettext("Show in overview")} - class="max-w-[9.375rem] text-center" - > - - {gettext("Yes")} - - - {gettext("No")} - - - - <:action :let={{_id, custom_field}}> - <.link phx-click={ - JS.push("edit_custom_field", value: %{id: custom_field.id}, target: @myself) - }> - {gettext("Edit")} - - - - <:action :let={{_id, custom_field}}> - <.link phx-click={ - JS.push("prepare_delete", value: %{id: custom_field.id}, target: @myself) - }> - {gettext("Delete")} - - - - - <%!-- Delete Confirmation Modal --%> - -
""" end diff --git a/lib/mv_web/live/global_settings_live.ex b/lib/mv_web/live/global_settings_live.ex index 87a1a4d..bd57d55 100644 --- a/lib/mv_web/live/global_settings_live.ex +++ b/lib/mv_web/live/global_settings_live.ex @@ -63,16 +63,18 @@ defmodule MvWeb.GlobalSettingsLive do <%!-- Memberdata Section --%> - <.live_component - module={MvWeb.MemberFieldLive.IndexComponent} - id="member-fields-component" - settings={@settings} - /> - <%!-- Custom Fields Section --%> - <.live_component - module={MvWeb.CustomFieldLive.IndexComponent} - id="custom-fields-component" - /> + <.form_section title={gettext("Memberdata")}> + <.live_component + module={MvWeb.MemberFieldLive.IndexComponent} + id="member-fields-component" + settings={@settings} + /> + <%!-- Custom Fields Section --%> + <.live_component + module={MvWeb.CustomFieldLive.IndexComponent} + id="custom-fields-component" + /> + """ end @@ -158,6 +160,24 @@ defmodule MvWeb.GlobalSettingsLive do {:noreply, put_flash(socket, :error, error_message)} end + @impl true + def handle_info({:member_field_saved, _member_field, action}, socket) do + # Reload settings to get updated member_field_visibility + {:ok, updated_settings} = Membership.get_settings() + + # Send update to member fields component to close form + send_update(MvWeb.MemberFieldLive.IndexComponent, + id: "member-fields-component", + show_form: false, + settings: updated_settings + ) + + {:noreply, + socket + |> assign(:settings, updated_settings) + |> put_flash(:info, gettext("Member field %{action} successfully", action: action))} + end + defp assign_form(%{assigns: %{settings: settings}} = socket) do form = AshPhoenix.Form.for_update( From e2c5971dafd904bfb2e9dda26b544e55ce819a42 Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 16 Dec 2025 17:16:44 +0100 Subject: [PATCH 08/23] chore: updates translation --- priv/gettext/de/LC_MESSAGES/default.po | 87 +++++++++++++++++++------- priv/gettext/default.pot | 67 +++++++++++++------- priv/gettext/en/LC_MESSAGES/default.po | 87 +++++++++++++++++++------- 3 files changed, 178 insertions(+), 63 deletions(-) diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 088304b..56f893d 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -44,6 +44,7 @@ msgstr "Löschen" #: lib/mv_web/live/contribution_type_live/index.ex #: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/user_live/form.ex #: lib/mv_web/live/user_live/index.html.heex @@ -97,7 +98,6 @@ msgstr "Nachname" msgid "New Member" msgstr "Neues Mitglied" -#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/user_live/index.html.heex #, elixir-autogen, elixir-format @@ -170,6 +170,7 @@ msgstr "Mitglied speichern" #: lib/mv_web/live/custom_field_live/form_component.ex #: lib/mv_web/live/custom_field_value_live/form.ex #: lib/mv_web/live/global_settings_live.ex +#: lib/mv_web/live/member_field_live/form_component.ex #: lib/mv_web/live/member_live/form.ex #: lib/mv_web/live/user_live/form.ex #, elixir-autogen, elixir-format @@ -258,6 +259,7 @@ msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt" #: lib/mv_web/live/custom_field_live/form_component.ex #: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/custom_field_value_live/form.ex +#: lib/mv_web/live/member_field_live/form_component.ex #: lib/mv_web/live/member_live/form.ex #: lib/mv_web/live/user_live/form.ex #, elixir-autogen, elixir-format @@ -271,6 +273,8 @@ msgstr "Mitglied auswählen" #: lib/mv_web/live/custom_field_live/form_component.ex #: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Description" msgstr "Beschreibung" @@ -286,6 +290,7 @@ msgid "Enabled" msgstr "Aktiviert" #: lib/mv_web/live/custom_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/form_component.ex #, elixir-autogen, elixir-format msgid "Immutable" msgstr "Unveränderlich" @@ -317,6 +322,8 @@ msgstr "Mitglieder" #: lib/mv_web/live/contribution_type_live/index.ex #: lib/mv_web/live/custom_field_live/form_component.ex #: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Name" msgstr "Name" @@ -349,6 +356,7 @@ msgstr "Profil" #: lib/mv_web/live/custom_field_live/form_component.ex #: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/form_component.ex #: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Required" @@ -407,6 +415,7 @@ msgid "Value" msgstr "Wert" #: lib/mv_web/live/custom_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/form_component.ex #, elixir-autogen, elixir-format msgid "Value type" msgstr "Wertetyp" @@ -673,6 +682,7 @@ msgstr "Um die Löschung zu bestätigen, gib bitte folgenden Text ein:" #: lib/mv_web/live/custom_field_live/form_component.ex #: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/form_component.ex #: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Show in overview" @@ -1415,10 +1425,13 @@ msgid "These will appear in addition to other data when adding new members." msgstr "Diese Felder können zusätzlich zu den normalen Daten ausgefüllt werden, wenn ein neues Mitglied angelegt wird." #: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format, fuzzy msgid "Value Type" msgstr "Wertetyp" +#: lib/mv_web/live/member_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/translations/field_types.ex #, elixir-autogen, elixir-format msgid "Date" @@ -1449,21 +1462,6 @@ msgstr "Ja/Nein-Auswahl" msgid "Failed to update member field visibility: %{error}" msgstr "Fehler beim anpassen der Sichtbarkeit des Feldes: %{error}" -#: lib/mv_web/live/member_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "Field Name" -msgstr "Name des Datenfelds" - -#: lib/mv_web/live/member_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "Hide" -msgstr "Ausblenden" - -#: lib/mv_web/live/member_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "Hide %{field} in overview" -msgstr "Verstecke %{field} in der Übersicht" - #: lib/mv_web/live/global_settings_live.ex #, elixir-autogen, elixir-format msgid "Member field visibility updated successfully" @@ -1480,16 +1478,43 @@ msgstr "Mitgliederdaten" msgid "Optional" msgstr "Optional" -#: lib/mv_web/live/member_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "Show %{field} in overview" -msgstr "" - #: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview." msgstr "Diese Datenfelder sind für MILA notwendig um Mitglieder zu identifizieren und zukünftig Beitragszahlungen zu berechnen. Aus diesem Grund können sie nicht gelöscht, aber in der Übersicht ausgeblendet werden." +#: lib/mv_web/live/member_field_live/form_component.ex +#, elixir-autogen, elixir-format +msgid "Back to member field overview" +msgstr "" + +#: lib/mv_web/live/member_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "Boolean" +msgstr "" + +#: lib/mv_web/live/member_field_live/form_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Edit Member Field: %{field}" +msgstr "Mitglied bearbeiten" + +#: lib/mv_web/live/global_settings_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Member field %{action} successfully" +msgstr "Mitglied wurde erfolgreich %{action}" + +#: lib/mv_web/live/member_field_live/form_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Save Member Field" +msgstr "Mitglied speichern" + +#: lib/mv_web/live/member_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "String" +msgstr "Einstellungen" + #~ #: lib/mv_web/live/custom_field_live/show.ex #~ #, elixir-autogen, elixir-format #~ msgid "Auto-generated identifier (immutable)" @@ -1507,11 +1532,26 @@ msgstr "Diese Datenfelder sind für MILA notwendig um Mitglieder zu identifizier #~ msgid "Custom Field Values" #~ msgstr "Benutzerdefinierte Feldwerte" +#~ #: lib/mv_web/live/member_field_live/index_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Field Name" +#~ msgstr "Name des Datenfelds" + #~ #: lib/mv_web/live/member_live/form.ex #~ #, elixir-autogen, elixir-format #~ msgid "Fields marked with an asterisk (*) cannot be empty." #~ msgstr "Felder, die mit einem Sternchen (*) markiert sind, dürfen nicht leer bleiben." +#~ #: lib/mv_web/live/member_field_live/index_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Hide" +#~ msgstr "Ausblenden" + +#~ #: lib/mv_web/live/member_field_live/index_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Hide %{field} in overview" +#~ msgstr "Verstecke %{field} in der Übersicht" + #~ #: lib/mv_web/live/custom_field_live/form.ex #~ #: lib/mv_web/live/user_live/show.ex #~ #, elixir-autogen, elixir-format @@ -1535,6 +1575,11 @@ msgstr "Diese Datenfelder sind für MILA notwendig um Mitglieder zu identifizier #~ msgid "OIDC ID" #~ msgstr "OIDC ID" +#~ #: lib/mv_web/live/member_field_live/index_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Show %{field} in overview" +#~ msgstr "" + #~ #: lib/mv_web/live/custom_field_live/index_component.ex #~ #, elixir-autogen, elixir-format, fuzzy #~ msgid "Show in Overview" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index 0677858..24dbcc7 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -45,6 +45,7 @@ msgstr "" #: lib/mv_web/live/contribution_type_live/index.ex #: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/user_live/form.ex #: lib/mv_web/live/user_live/index.html.heex @@ -98,7 +99,6 @@ msgstr "" msgid "New Member" msgstr "" -#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/user_live/index.html.heex #, elixir-autogen, elixir-format @@ -171,6 +171,7 @@ msgstr "" #: lib/mv_web/live/custom_field_live/form_component.ex #: lib/mv_web/live/custom_field_value_live/form.ex #: lib/mv_web/live/global_settings_live.ex +#: lib/mv_web/live/member_field_live/form_component.ex #: lib/mv_web/live/member_live/form.ex #: lib/mv_web/live/user_live/form.ex #, elixir-autogen, elixir-format @@ -259,6 +260,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/custom_field_value_live/form.ex +#: lib/mv_web/live/member_field_live/form_component.ex #: lib/mv_web/live/member_live/form.ex #: lib/mv_web/live/user_live/form.ex #, elixir-autogen, elixir-format @@ -272,6 +274,8 @@ 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/member_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Description" msgstr "" @@ -287,6 +291,7 @@ msgid "Enabled" msgstr "" #: lib/mv_web/live/custom_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/form_component.ex #, elixir-autogen, elixir-format msgid "Immutable" msgstr "" @@ -318,6 +323,8 @@ msgstr "" #: lib/mv_web/live/contribution_type_live/index.ex #: lib/mv_web/live/custom_field_live/form_component.ex #: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Name" msgstr "" @@ -350,6 +357,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/member_field_live/form_component.ex #: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Required" @@ -408,6 +416,7 @@ msgid "Value" msgstr "" #: lib/mv_web/live/custom_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/form_component.ex #, elixir-autogen, elixir-format msgid "Value type" msgstr "" @@ -674,6 +683,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/member_field_live/form_component.ex #: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Show in overview" @@ -1416,10 +1426,13 @@ msgid "These will appear in addition to other data when adding new members." msgstr "" #: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Value Type" msgstr "" +#: lib/mv_web/live/member_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/translations/field_types.ex #, elixir-autogen, elixir-format msgid "Date" @@ -1450,21 +1463,6 @@ msgstr "" msgid "Failed to update member field visibility: %{error}" msgstr "" -#: lib/mv_web/live/member_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "Field Name" -msgstr "" - -#: lib/mv_web/live/member_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "Hide" -msgstr "" - -#: lib/mv_web/live/member_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "Hide %{field} in overview" -msgstr "" - #: lib/mv_web/live/global_settings_live.ex #, elixir-autogen, elixir-format msgid "Member field visibility updated successfully" @@ -1481,12 +1479,39 @@ msgstr "" msgid "Optional" msgstr "" -#: lib/mv_web/live/member_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "Show %{field} in overview" -msgstr "" - #: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview." msgstr "" + +#: lib/mv_web/live/member_field_live/form_component.ex +#, elixir-autogen, elixir-format +msgid "Back to member field overview" +msgstr "" + +#: lib/mv_web/live/member_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "Boolean" +msgstr "" + +#: lib/mv_web/live/member_field_live/form_component.ex +#, elixir-autogen, elixir-format +msgid "Edit Member Field: %{field}" +msgstr "" + +#: lib/mv_web/live/global_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Member field %{action} successfully" +msgstr "" + +#: lib/mv_web/live/member_field_live/form_component.ex +#, elixir-autogen, elixir-format +msgid "Save Member Field" +msgstr "" + +#: lib/mv_web/live/member_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "String" +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 131f4dc..5a32e01 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -45,6 +45,7 @@ msgstr "" #: lib/mv_web/live/contribution_type_live/index.ex #: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/user_live/form.ex #: lib/mv_web/live/user_live/index.html.heex @@ -98,7 +99,6 @@ msgstr "" msgid "New Member" msgstr "" -#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/user_live/index.html.heex #, elixir-autogen, elixir-format @@ -171,6 +171,7 @@ msgstr "" #: lib/mv_web/live/custom_field_live/form_component.ex #: lib/mv_web/live/custom_field_value_live/form.ex #: lib/mv_web/live/global_settings_live.ex +#: lib/mv_web/live/member_field_live/form_component.ex #: lib/mv_web/live/member_live/form.ex #: lib/mv_web/live/user_live/form.ex #, elixir-autogen, elixir-format @@ -259,6 +260,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/custom_field_value_live/form.ex +#: lib/mv_web/live/member_field_live/form_component.ex #: lib/mv_web/live/member_live/form.ex #: lib/mv_web/live/user_live/form.ex #, elixir-autogen, elixir-format @@ -272,6 +274,8 @@ 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/member_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Description" msgstr "" @@ -287,6 +291,7 @@ msgid "Enabled" msgstr "" #: lib/mv_web/live/custom_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/form_component.ex #, elixir-autogen, elixir-format msgid "Immutable" msgstr "" @@ -318,6 +323,8 @@ msgstr "" #: lib/mv_web/live/contribution_type_live/index.ex #: lib/mv_web/live/custom_field_live/form_component.ex #: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Name" msgstr "" @@ -350,6 +357,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/member_field_live/form_component.ex #: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Required" @@ -408,6 +416,7 @@ msgid "Value" msgstr "" #: lib/mv_web/live/custom_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/form_component.ex #, elixir-autogen, elixir-format msgid "Value type" msgstr "" @@ -674,6 +683,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/member_field_live/form_component.ex #: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Show in overview" @@ -1416,10 +1426,13 @@ msgid "These will appear in addition to other data when adding new members." msgstr "" #: lib/mv_web/live/custom_field_live/index_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format, fuzzy msgid "Value Type" msgstr "" +#: lib/mv_web/live/member_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/translations/field_types.ex #, elixir-autogen, elixir-format msgid "Date" @@ -1450,21 +1463,6 @@ msgstr "" msgid "Failed to update member field visibility: %{error}" msgstr "" -#: lib/mv_web/live/member_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "Field Name" -msgstr "" - -#: lib/mv_web/live/member_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "Hide" -msgstr "" - -#: lib/mv_web/live/member_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "Hide %{field} in overview" -msgstr "" - #: lib/mv_web/live/global_settings_live.ex #, elixir-autogen, elixir-format msgid "Member field visibility updated successfully" @@ -1481,16 +1479,43 @@ msgstr "" msgid "Optional" msgstr "" -#: lib/mv_web/live/member_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "Show %{field} in overview" -msgstr "" - #: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format, fuzzy msgid "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview." msgstr "" +#: lib/mv_web/live/member_field_live/form_component.ex +#, elixir-autogen, elixir-format +msgid "Back to member field overview" +msgstr "" + +#: lib/mv_web/live/member_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format +msgid "Boolean" +msgstr "" + +#: lib/mv_web/live/member_field_live/form_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Edit Member Field: %{field}" +msgstr "" + +#: lib/mv_web/live/global_settings_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Member field %{action} successfully" +msgstr "" + +#: lib/mv_web/live/member_field_live/form_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Save Member Field" +msgstr "" + +#: lib/mv_web/live/member_field_live/form_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "String" +msgstr "" + #~ #: lib/mv_web/live/custom_field_live/show.ex #~ #, elixir-autogen, elixir-format #~ msgid "Auto-generated identifier (immutable)" @@ -1508,11 +1533,26 @@ msgstr "" #~ msgid "Custom Field Values" #~ msgstr "" +#~ #: lib/mv_web/live/member_field_live/index_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Field Name" +#~ msgstr "" + #~ #: lib/mv_web/live/member_live/form.ex #~ #, elixir-autogen, elixir-format #~ msgid "Fields marked with an asterisk (*) cannot be empty." #~ msgstr "" +#~ #: lib/mv_web/live/member_field_live/index_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Hide" +#~ msgstr "" + +#~ #: lib/mv_web/live/member_field_live/index_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Hide %{field} in overview" +#~ msgstr "" + #~ #: lib/mv_web/live/user_live/show.ex #~ #, elixir-autogen, elixir-format #~ msgid "ID" @@ -1534,6 +1574,11 @@ msgstr "" #~ msgid "OIDC ID" #~ msgstr "" +#~ #: lib/mv_web/live/member_field_live/index_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Show %{field} in overview" +#~ msgstr "" + #~ #: lib/mv_web/live/custom_field_live/index_component.ex #~ #, elixir-autogen, elixir-format, fuzzy #~ msgid "Show in Overview" From cbe05c5ca85b5a721062424527049f215d6d828c Mon Sep 17 00:00:00 2001 From: carla Date: Wed, 7 Jan 2026 12:03:58 +0100 Subject: [PATCH 09/23] fix: cath all rauthy errors --- lib/mv_web/controllers/auth_controller.ex | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/mv_web/controllers/auth_controller.ex b/lib/mv_web/controllers/auth_controller.ex index 9282903..20a8b20 100644 --- a/lib/mv_web/controllers/auth_controller.ex +++ b/lib/mv_web/controllers/auth_controller.ex @@ -78,6 +78,12 @@ defmodule MvWeb.AuthController do end end + # Catch-all clause for any other error types + defp handle_rauthy_failure(conn, reason) do + Logger.warning("Unhandled Rauthy failure reason: #{inspect(reason)}") + redirect_with_error(conn, gettext("Unable to authenticate with OIDC. Please try again.")) + end + # Handle generic AuthenticationFailed errors defp handle_authentication_failed(conn, %Ash.Error.Forbidden{errors: errors}) do if Enum.any?(errors, &match?(%AshAuthentication.Errors.CannotConfirmUnconfirmedUser{}, &1)) do From 38d106a69e9227985db14dd22a00d1c78419feee Mon Sep 17 00:00:00 2001 From: carla Date: Wed, 7 Jan 2026 12:14:41 +0100 Subject: [PATCH 10/23] fix: exit date as default hidden column --- lib/membership/member.ex | 11 +++++++---- lib/membership/membership.ex | 5 ++++- .../live/member_field_live/index_component.ex | 13 +++++++++---- lib/mv_web/live/member_live/index.html.heex | 18 ++++++++++++++++++ .../live/member_live/index/field_visibility.ex | 4 +++- 5 files changed, 41 insertions(+), 10 deletions(-) diff --git a/lib/membership/member.ex b/lib/membership/member.ex index 6ae9307..51da8ff 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -600,18 +600,21 @@ defmodule Mv.Membership.Member do """ @spec show_in_overview?(atom()) :: boolean() def show_in_overview?(field) when is_atom(field) do + # exit_date defaults to false (hidden) instead of true + default_visibility = if field == :exit_date, do: false, else: true + case Mv.Membership.get_settings() do {:ok, settings} -> visibility_config = settings.member_field_visibility || %{} # Normalize map keys to atoms (JSONB may return string keys) normalized_config = normalize_visibility_config(visibility_config) - # Get value from normalized config, default to true - Map.get(normalized_config, field, true) + # Get value from normalized config, use field-specific default + Map.get(normalized_config, field, default_visibility) {:error, _} -> - # If settings can't be loaded, default to visible - true + # If settings can't be loaded, use field-specific default + default_visibility end end diff --git a/lib/membership/membership.ex b/lib/membership/membership.ex index 4917c7c..c711bcd 100644 --- a/lib/membership/membership.ex +++ b/lib/membership/membership.ex @@ -89,7 +89,10 @@ defmodule Mv.Membership do default_club_name = System.get_env("ASSOCIATION_NAME") || "Club Name" Mv.Membership.Setting - |> Ash.Changeset.for_create(:create, %{club_name: default_club_name}) + |> Ash.Changeset.for_create(:create, %{ + club_name: default_club_name, + member_field_visibility: %{"exit_date" => false} + }) |> Ash.create!(domain: __MODULE__) |> then(fn settings -> {:ok, settings} end) diff --git a/lib/mv_web/live/member_field_live/index_component.ex b/lib/mv_web/live/member_field_live/index_component.ex index 7422f5a..eec98be 100644 --- a/lib/mv_web/live/member_field_live/index_component.ex +++ b/lib/mv_web/live/member_field_live/index_component.ex @@ -5,7 +5,7 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do ## Features - List all member fields from Mv.Constants.member_fields() - Display show_in_overview status as badge (Yes/No) - - Display required status for required fields (first_name, last_name, email) + - Display required status based on actual attribute definitions (allow_nil? false) - Edit member field properties (expandable form like custom fields) - Updates Settings.member_field_visibility """ @@ -15,8 +15,6 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do alias MvWeb.Translations.MemberFields alias MvWeb.Translations.FieldTypes - @required_fields [:first_name, :last_name, :email] - @impl true def render(assigns) do assigns = @@ -245,6 +243,13 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do defp normalize_visibility_config(_), do: %{} - defp required?(field) when field in @required_fields, do: true + # Check if a field is required by checking the actual attribute definition + defp required?(field) when is_atom(field) do + case Ash.Resource.Info.attribute(Mv.Membership.Member, field) do + nil -> false + attribute -> not attribute.allow_nil? + end + end + defp required?(_), do: false end diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index 1557ed9..b2af205 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -257,6 +257,24 @@ > {MvWeb.MemberLive.Index.format_date(member.join_date)} + <: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 :let={member} label={gettext("Membership Fee Status")} diff --git a/lib/mv_web/live/member_live/index/field_visibility.ex b/lib/mv_web/live/member_live/index/field_visibility.ex index c9c8bd6..627bbcf 100644 --- a/lib/mv_web/live/member_live/index/field_visibility.ex +++ b/lib/mv_web/live/member_live/index/field_visibility.ex @@ -183,7 +183,9 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do Enum.reduce(member_fields, %{}, fn field, acc -> field_string = Atom.to_string(field) - show_in_overview = Map.get(visibility_config, field, true) + # exit_date defaults to false (hidden), all other fields default to true + default_visibility = if field == :exit_date, do: false, else: true + show_in_overview = Map.get(visibility_config, field, default_visibility) Map.put(acc, field_string, show_in_overview) end) end From 4a6e7cf51a58cf5d03e18c8c57e948a617b68d8c Mon Sep 17 00:00:00 2001 From: carla Date: Wed, 7 Jan 2026 18:11:07 +0100 Subject: [PATCH 11/23] feat: show only edit or list view in settings --- .../live/custom_field_live/index_component.ex | 20 +++++++++++++++++++ lib/mv_web/live/global_settings_live.ex | 13 +++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/mv_web/live/custom_field_live/index_component.ex b/lib/mv_web/live/custom_field_live/index_component.ex index ee8e573..a11cc57 100644 --- a/lib/mv_web/live/custom_field_live/index_component.ex +++ b/lib/mv_web/live/custom_field_live/index_component.ex @@ -178,6 +178,9 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do @impl true def update(assigns, socket) do + # Track previous show_form state to detect when form is closed + previous_show_form = Map.get(socket.assigns, :show_form, false) + # If show_form is explicitly provided in assigns, reset editing state socket = if Map.has_key?(assigns, :show_form) and assigns.show_form == false do @@ -188,6 +191,13 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do socket end + # Detect when form is closed (show_form changes from true to false) + new_show_form = Map.get(assigns, :show_form, false) + + if previous_show_form and not new_show_form do + send(self(), {:editing_section_changed, nil}) + end + {:ok, socket |> assign(assigns) @@ -202,6 +212,11 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do @impl true def handle_event("new_custom_field", _params, socket) do + # Only send event if form was not already open + if not socket.assigns[:show_form] do + send(self(), {:editing_section_changed, :custom_fields}) + end + {:noreply, socket |> assign(:show_form, true) @@ -213,6 +228,11 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do def handle_event("edit_custom_field", %{"id" => id}, socket) do custom_field = Ash.get!(Mv.Membership.CustomField, id) + # Only send event if form was not already open + if not socket.assigns[:show_form] do + send(self(), {:editing_section_changed, :custom_fields}) + end + {:noreply, socket |> assign(:show_form, true) diff --git a/lib/mv_web/live/global_settings_live.ex b/lib/mv_web/live/global_settings_live.ex index 6f7bb54..2798412 100644 --- a/lib/mv_web/live/global_settings_live.ex +++ b/lib/mv_web/live/global_settings_live.ex @@ -31,6 +31,7 @@ defmodule MvWeb.GlobalSettingsLive do socket |> assign(:page_title, gettext("Settings")) |> assign(:settings, settings) + |> assign(:active_editing_section, nil) |> assign_form()} end @@ -65,12 +66,14 @@ defmodule MvWeb.GlobalSettingsLive do <%!-- Memberdata Section --%> <.form_section title={gettext("Memberdata")}> <.live_component + :if={@active_editing_section != :custom_fields} module={MvWeb.MemberFieldLive.IndexComponent} id="member-fields-component" settings={@settings} /> <%!-- Custom Fields Section --%> <.live_component + :if={@active_editing_section != :member_fields} module={MvWeb.CustomFieldLive.IndexComponent} id="custom-fields-component" /> @@ -113,7 +116,9 @@ defmodule MvWeb.GlobalSettingsLive do ) {:noreply, - put_flash(socket, :info, gettext("Custom field %{action} successfully", action: action))} + socket + |> assign(:active_editing_section, nil) + |> put_flash(:info, gettext("Custom field %{action} successfully", action: action))} end @impl true @@ -163,6 +168,11 @@ defmodule MvWeb.GlobalSettingsLive do {:noreply, put_flash(socket, :error, error_message)} end + @impl true + def handle_info({:editing_section_changed, section}, socket) do + {:noreply, assign(socket, :active_editing_section, section)} + end + @impl true def handle_info({:member_field_saved, _member_field, action}, socket) do # Reload settings to get updated member_field_visibility @@ -178,6 +188,7 @@ defmodule MvWeb.GlobalSettingsLive do {:noreply, socket |> assign(:settings, updated_settings) + |> assign(:active_editing_section, nil) |> put_flash(:info, gettext("Member field %{action} successfully", action: action))} end From 36776f8e287a2680177cd571896e66709f850b53 Mon Sep 17 00:00:00 2001 From: carla Date: Wed, 7 Jan 2026 18:11:36 +0100 Subject: [PATCH 12/23] fix tests and linting --- lib/mv_web/helpers/membership_fee_helpers.ex | 2 +- .../live/member_field_live/form_component.ex | 2 +- .../live/member_field_live/index_component.ex | 17 +- lib/mv_web/live/member_live/index.ex | 2 +- .../show/membership_fees_component.ex | 6 +- .../live/membership_fee_type_live/form.ex | 2 +- .../live/membership_fee_type_live/index.ex | 4 +- priv/repo/seeds.exs | 35 +++- .../member_field_visibility_test.exs | 9 +- .../index_component_test.exs | 66 -------- .../index_required_display_test.exs | 157 ------------------ 11 files changed, 63 insertions(+), 239 deletions(-) delete mode 100644 test/mv_web/member_live/index_required_display_test.exs diff --git a/lib/mv_web/helpers/membership_fee_helpers.ex b/lib/mv_web/helpers/membership_fee_helpers.ex index 53d32c7..4986ca6 100644 --- a/lib/mv_web/helpers/membership_fee_helpers.ex +++ b/lib/mv_web/helpers/membership_fee_helpers.ex @@ -8,9 +8,9 @@ defmodule MvWeb.Helpers.MembershipFeeHelpers do use Gettext, backend: MvWeb.Gettext + alias Mv.Membership.Member alias Mv.MembershipFees.CalendarCycles alias Mv.MembershipFees.MembershipFeeCycle - alias Mv.Membership.Member @doc """ Formats a decimal amount as currency string. diff --git a/lib/mv_web/live/member_field_live/form_component.ex b/lib/mv_web/live/member_field_live/form_component.ex index a9985cb..0f0b446 100644 --- a/lib/mv_web/live/member_field_live/form_component.ex +++ b/lib/mv_web/live/member_field_live/form_component.ex @@ -18,8 +18,8 @@ defmodule MvWeb.MemberFieldLive.FormComponent do use MvWeb, :live_component alias Mv.Membership - alias MvWeb.Translations.MemberFields alias MvWeb.Translations.FieldTypes + alias MvWeb.Translations.MemberFields @required_fields [:first_name, :last_name, :email] diff --git a/lib/mv_web/live/member_field_live/index_component.ex b/lib/mv_web/live/member_field_live/index_component.ex index eec98be..2d4f1dc 100644 --- a/lib/mv_web/live/member_field_live/index_component.ex +++ b/lib/mv_web/live/member_field_live/index_component.ex @@ -12,8 +12,8 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do use MvWeb, :live_component alias Mv.Membership - alias MvWeb.Translations.MemberFields alias MvWeb.Translations.FieldTypes + alias MvWeb.Translations.MemberFields @impl true def render(assigns) do @@ -109,6 +109,9 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do @impl true def update(assigns, socket) do + # Track previous show_form state to detect when form is closed + previous_show_form = Map.get(socket.assigns, :show_form, false) + # If show_form is explicitly provided in assigns, reset editing state socket = if Map.has_key?(assigns, :show_form) and assigns.show_form == false do @@ -119,6 +122,13 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do socket end + # Detect when form is closed (show_form changes from true to false) + new_show_form = Map.get(assigns, :show_form, false) + + if previous_show_form and not new_show_form do + send(self(), {:editing_section_changed, nil}) + end + {:ok, socket |> assign(assigns) @@ -136,6 +146,11 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do if field_string in valid_fields do field_atom = String.to_existing_atom(field_string) + # Only send event if form was not already open + if not socket.assigns[:show_form] do + send(self(), {:editing_section_changed, :member_fields}) + end + {:noreply, socket |> assign(:show_form, true) diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index fff5517..34928cd 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -31,10 +31,10 @@ defmodule MvWeb.MemberLive.Index do import Ash.Expr alias Mv.Membership - alias MvWeb.MemberLive.Index.Formatter alias MvWeb.Helpers.DateFormatter alias MvWeb.MemberLive.Index.FieldSelection alias MvWeb.MemberLive.Index.FieldVisibility + alias MvWeb.MemberLive.Index.Formatter alias MvWeb.MemberLive.Index.MembershipFeeStatus # Prefix used in sort field names for custom fields (e.g., "custom_field_") diff --git a/lib/mv_web/live/member_live/show/membership_fees_component.ex b/lib/mv_web/live/member_live/show/membership_fees_component.ex index f96fd73..0bc93a1 100644 --- a/lib/mv_web/live/member_live/show/membership_fees_component.ex +++ b/lib/mv_web/live/member_live/show/membership_fees_component.ex @@ -15,10 +15,10 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do require Ash.Query alias Mv.Membership - alias Mv.MembershipFees.MembershipFeeType - alias Mv.MembershipFees.MembershipFeeCycle - alias Mv.MembershipFees.CycleGenerator alias Mv.MembershipFees.CalendarCycles + alias Mv.MembershipFees.CycleGenerator + alias Mv.MembershipFees.MembershipFeeCycle + alias Mv.MembershipFees.MembershipFeeType alias MvWeb.Helpers.MembershipFeeHelpers @impl true diff --git a/lib/mv_web/live/membership_fee_type_live/form.ex b/lib/mv_web/live/membership_fee_type_live/form.ex index 5acb8c9..77a73af 100644 --- a/lib/mv_web/live/membership_fee_type_live/form.ex +++ b/lib/mv_web/live/membership_fee_type_live/form.ex @@ -15,9 +15,9 @@ defmodule MvWeb.MembershipFeeTypeLive.Form do require Ash.Query + alias Mv.Membership.Member alias Mv.MembershipFees alias Mv.MembershipFees.MembershipFeeType - alias Mv.Membership.Member alias MvWeb.Helpers.MembershipFeeHelpers @impl true diff --git a/lib/mv_web/live/membership_fee_type_live/index.ex b/lib/mv_web/live/membership_fee_type_live/index.ex index 176d4e1..262983f 100644 --- a/lib/mv_web/live/membership_fee_type_live/index.ex +++ b/lib/mv_web/live/membership_fee_type_live/index.ex @@ -16,10 +16,10 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do require Ash.Query - alias Mv.MembershipFees - alias Mv.MembershipFees.MembershipFeeType alias Mv.Membership alias Mv.Membership.Member + alias Mv.MembershipFees + alias Mv.MembershipFees.MembershipFeeType alias MvWeb.Helpers.MembershipFeeHelpers @impl true diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 4f99e5b..9bbcff3 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -491,10 +491,39 @@ default_club_name = System.get_env("ASSOCIATION_NAME") || "Club Name" case Membership.get_settings() do {:ok, existing_settings} -> # Settings exist, update if club_name is different from env var - if existing_settings.club_name != default_club_name do - {:ok, _updated} = - Membership.update_settings(existing_settings, %{club_name: default_club_name}) + # Also ensure exit_date is set to false by default if not already configured + updates = + %{} + |> then(fn acc -> + if existing_settings.club_name != default_club_name, + do: Map.put(acc, :club_name, default_club_name), + else: acc + end) + |> then(fn acc -> + visibility_config = existing_settings.member_field_visibility || %{} + # Ensure exit_date is set to false if not already configured + if not Map.has_key?(visibility_config, "exit_date") and + not Map.has_key?(visibility_config, :exit_date) do + updated_visibility = Map.put(visibility_config, "exit_date", false) + Map.put(acc, :member_field_visibility, updated_visibility) + else + acc + end + end) + + if map_size(updates) > 0 do + {:ok, _updated} = Membership.update_settings(existing_settings, updates) end + + {:ok, nil} -> + # Settings don't exist yet, create with exit_date defaulting to false + {:ok, _settings} = + Membership.Setting + |> Ash.Changeset.for_create(:create, %{ + club_name: default_club_name, + member_field_visibility: %{"exit_date" => false} + }) + |> Ash.create!() end IO.puts("✅ Seeds completed successfully!") diff --git a/test/membership/member_field_visibility_test.exs b/test/membership/member_field_visibility_test.exs index 9c7e5e0..6bc04f6 100644 --- a/test/membership/member_field_visibility_test.exs +++ b/test/membership/member_field_visibility_test.exs @@ -13,14 +13,17 @@ defmodule Mv.Membership.MemberFieldVisibilityTest do alias Mv.Membership.Member describe "show_in_overview?/1" do - test "returns true for all member fields by default" do + test "returns true for all member fields by default, except exit_date" do # When no settings exist or member_field_visibility is not configured # Test with fields from constants + # Note: exit_date defaults to false (hidden) by design member_fields = Mv.Constants.member_fields() Enum.each(member_fields, fn field -> - assert Member.show_in_overview?(field) == true, - "Field #{field} should be visible by default" + expected_visibility = if field == :exit_date, do: false, else: true + + assert Member.show_in_overview?(field) == expected_visibility, + "Field #{field} should be #{if expected_visibility, do: "visible", else: "hidden"} by default" end) end diff --git a/test/mv_web/live/member_field_live/index_component_test.exs b/test/mv_web/live/member_field_live/index_component_test.exs index e2e1be3..037a77c 100644 --- a/test/mv_web/live/member_field_live/index_component_test.exs +++ b/test/mv_web/live/member_field_live/index_component_test.exs @@ -6,8 +6,6 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do - Rendering all member fields from Mv.Constants.member_fields() - Displaying show_in_overview status as badge (Yes/No) - Displaying required status for required fields (first_name, last_name, email) - - Toggle functionality to change show_in_overview flag - - Settings are correctly updated after toggle - Current status is displayed based on settings.member_field_visibility - Default status is "Yes" (visible) when not configured in settings """ @@ -86,70 +84,6 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do end end - describe "toggle functionality" do - test "toggles field visibility from visible to hidden", %{conn: conn} do - # Start with field visible (default) - {:ok, settings} = Membership.get_settings() - - {:ok, _updated} = - Membership.update_member_field_visibility(settings, %{"street" => true}) - - {:ok, view, _html} = live(conn, ~p"/settings") - - # Find and click toggle button for street field - # This will fail until component is implemented - assert has_element?(view, "#member-field-street-toggle") or - has_element?(view, "[phx-click='toggle_field_visibility'][data-field='street']") - - # Click toggle - view - |> element("#member-field-street-toggle") - |> render_click(%{"field" => "street"}) - - # Verify settings updated - {:ok, updated_settings} = Membership.get_settings() - visibility = updated_settings.member_field_visibility || %{} - assert Map.get(visibility, "street") == false - end - - test "toggles field visibility from hidden to visible", %{conn: conn} do - # Start with field hidden - {:ok, settings} = Membership.get_settings() - - {:ok, _updated} = - Membership.update_member_field_visibility(settings, %{"street" => false}) - - {:ok, view, _html} = live(conn, ~p"/settings") - - # Click toggle to make visible - view - |> element("#member-field-street-toggle") - |> render_click(%{"field" => "street"}) - - # Verify settings updated - {:ok, updated_settings} = Membership.get_settings() - visibility = updated_settings.member_field_visibility || %{} - assert Map.get(visibility, "street") == true - end - - test "sends message to parent LiveView after toggle", %{conn: conn} do - {:ok, settings} = Membership.get_settings() - - {:ok, _updated} = - Membership.update_member_field_visibility(settings, %{"street" => true}) - - {:ok, view, _html} = live(conn, ~p"/settings") - - # Toggle field - view - |> element("#member-field-street-toggle") - |> render_click(%{"field" => "street"}) - - # Check for flash message (handled by parent LiveView) - assert render(view) =~ "updated" or render(view) =~ "success" - end - end - describe "required fields" do test "marks first_name as required", %{conn: conn} do {:ok, _view, html} = live(conn, ~p"/settings") diff --git a/test/mv_web/member_live/index_required_display_test.exs b/test/mv_web/member_live/index_required_display_test.exs deleted file mode 100644 index eb61fea..0000000 --- a/test/mv_web/member_live/index_required_display_test.exs +++ /dev/null @@ -1,157 +0,0 @@ -defmodule MvWeb.MemberLive.IndexRequiredDisplayTest do - @moduledoc """ - Tests for displaying "required" badge in member overview. - - Tests cover: - - "required" badge for required member fields (first_name, last_name, email) - - "required" badge for required custom fields - - No "required" badge for optional member fields - - No "required" badge for optional custom fields - - Badge is positioned in column header - """ - # async: false to prevent PostgreSQL deadlocks when creating members and custom fields - use MvWeb.ConnCase, async: false - import Phoenix.LiveViewTest - require Ash.Query - - alias Mv.Membership.{CustomField, CustomFieldValue, Member} - - setup do - # Create test member - {:ok, member} = - Member - |> Ash.Changeset.for_create(:create_member, %{ - first_name: "Alice", - last_name: "Anderson", - email: "alice@example.com" - }) - |> Ash.create() - - # Create required custom field - {:ok, required_field} = - CustomField - |> Ash.Changeset.for_create(:create, %{ - name: "emergency_contact", - value_type: :string, - required: true, - show_in_overview: true - }) - |> Ash.create() - - # Create optional custom field - {:ok, optional_field} = - CustomField - |> Ash.Changeset.for_create(:create, %{ - name: "hobby", - value_type: :string, - required: false, - show_in_overview: true - }) - |> Ash.create() - - # Create custom field values - {:ok, _cfv1} = - CustomFieldValue - |> Ash.Changeset.for_create(:create, %{ - member_id: member.id, - custom_field_id: required_field.id, - value: %{"_union_type" => "string", "_union_value" => "John Doe"} - }) - |> Ash.create() - - {:ok, _cfv2} = - CustomFieldValue - |> Ash.Changeset.for_create(:create, %{ - member_id: member.id, - custom_field_id: optional_field.id, - value: %{"_union_type" => "string", "_union_value" => "Reading"} - }) - |> Ash.create() - - %{ - member: member, - required_field: required_field, - optional_field: optional_field - } - end - - describe "required badge for member fields" do - test "displays required badge for first_name column", %{conn: conn} do - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, "/members") - - # Check that first_name column header has required badge - assert html =~ "first_name" or html =~ "First name" or html =~ "First Name" - # Should have required indicator in header - assert html =~ "required" or html =~ "Required" - end - - test "displays required badge for last_name column", %{conn: conn} do - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, "/members") - - # Check that last_name column header has required badge - assert html =~ "last_name" or html =~ "Last name" or html =~ "Last Name" - # Should have required indicator in header - assert html =~ "required" or html =~ "Required" - end - - test "displays required badge for email column", %{conn: conn} do - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, "/members") - - # Check that email column header has required badge - assert html =~ "email" or html =~ "Email" - # Should have required indicator in header - assert html =~ "required" or html =~ "Required" - end - - test "does not display required badge for optional member fields", %{conn: conn} do - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, "/members") - - # Optional fields: street, city, phone_number, etc. - # These should not have required badge - # We check that street is present but doesn't have required indicator nearby - assert html =~ "street" or html =~ "Street" - end - end - - describe "required badge for custom fields" do - test "displays required badge for required custom field column", %{ - conn: conn, - required_field: field - } do - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, "/members") - - # Check that required custom field column header has required badge - assert html =~ field.name - # Should have required indicator in header - assert html =~ "required" or html =~ "Required" - end - - test "does not display required badge for optional custom field column", %{ - conn: conn, - optional_field: field - } do - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, "/members") - - # Check that optional custom field column header does not have required badge - assert html =~ field.name - # Should not have required indicator (or it should be clear it's optional) - end - end - - describe "badge positioning" do - test "required badge is in column header, not in cell content", %{conn: conn} do - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, "/members") - - # Required badge should be in thead (header), not in tbody (data rows) - # This is verified by checking that required appears near column headers - assert html =~ "thead" or html =~ "th" - end - end -end From 9af73818439787fa4cf1ef485e4c5d32645eaf81 Mon Sep 17 00:00:00 2001 From: carla Date: Thu, 8 Jan 2026 11:22:44 +0100 Subject: [PATCH 13/23] refactor: extract helper modules to remove code duplication --- lib/mv/helpers/type_parsers.ex | 49 +++++++++++++++ .../membership/helpers/visibility_config.ex | 55 +++++++++++++++++ lib/mv_web/helpers/field_type_formatter.ex | 59 +++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 lib/mv/helpers/type_parsers.ex create mode 100644 lib/mv/membership/helpers/visibility_config.ex create mode 100644 lib/mv_web/helpers/field_type_formatter.ex diff --git a/lib/mv/helpers/type_parsers.ex b/lib/mv/helpers/type_parsers.ex new file mode 100644 index 0000000..6c07e6e --- /dev/null +++ b/lib/mv/helpers/type_parsers.ex @@ -0,0 +1,49 @@ +defmodule Mv.Helpers.TypeParsers do + @moduledoc """ + Helper functions for parsing various input types to common Elixir types. + + Provides safe parsing functions for common type conversions, especially useful + when dealing with form data or external APIs. + """ + + @doc """ + Parses various input types to boolean. + + Handles: booleans, strings ("true"/"false"), integers (1/0), and other values (defaults to false). + + ## Parameters + + - `value` - The value to parse (boolean, string, integer, or other) + + ## Returns + + A boolean value + + ## Examples + + iex> parse_boolean(true) + true + + iex> parse_boolean("true") + true + + iex> parse_boolean("false") + false + + iex> parse_boolean(1) + true + + iex> parse_boolean(0) + false + + iex> parse_boolean(nil) + false + """ + @spec parse_boolean(any()) :: boolean() + def parse_boolean(value) when is_boolean(value), do: value + def parse_boolean("true"), do: true + def parse_boolean("false"), do: false + def parse_boolean(1), do: true + def parse_boolean(0), do: false + def parse_boolean(_), do: false +end diff --git a/lib/mv/membership/helpers/visibility_config.ex b/lib/mv/membership/helpers/visibility_config.ex new file mode 100644 index 0000000..886d575 --- /dev/null +++ b/lib/mv/membership/helpers/visibility_config.ex @@ -0,0 +1,55 @@ +defmodule Mv.Membership.Helpers.VisibilityConfig do + @moduledoc """ + Helper functions for normalizing member field visibility configuration. + + Handles conversion between string keys (from JSONB) and atom keys (Elixir convention). + JSONB in PostgreSQL converts atom keys to string keys when storing. + This module provides functions to normalize these back to atoms for Elixir usage. + """ + + @doc """ + Normalizes visibility config map keys from strings to atoms. + + JSONB in PostgreSQL converts atom keys to string keys when storing. + This function converts them back to atoms for Elixir usage. + + ## Parameters + + - `config` - A map with either string or atom keys + + ## Returns + + A map with atom keys (where possible) + + ## Examples + + iex> normalize(%{"first_name" => true, "email" => false}) + %{first_name: true, email: false} + + iex> normalize(%{first_name: true, email: false}) + %{first_name: true, email: false} + + iex> normalize(%{"invalid_field" => true}) + %{} + """ + @spec normalize(map()) :: map() + def normalize(config) when is_map(config) do + Enum.reduce(config, %{}, fn + {key, value}, acc when is_atom(key) -> + Map.put(acc, key, value) + + {key, value}, acc when is_binary(key) -> + try do + atom_key = String.to_existing_atom(key) + Map.put(acc, atom_key, value) + rescue + ArgumentError -> acc + end + + _, acc -> + acc + end) + end + + def normalize(_), do: %{} +end diff --git a/lib/mv_web/helpers/field_type_formatter.ex b/lib/mv_web/helpers/field_type_formatter.ex new file mode 100644 index 0000000..6cc86e6 --- /dev/null +++ b/lib/mv_web/helpers/field_type_formatter.ex @@ -0,0 +1,59 @@ +defmodule MvWeb.Helpers.FieldTypeFormatter do + @moduledoc """ + Helper functions for formatting field types for display. + + Handles both Ash type modules (e.g., `Ash.Type.String`) and simple atoms (e.g., `:string`). + """ + + alias MvWeb.Translations.FieldTypes + + @doc """ + Formats an Ash type for display. + + Handles both Ash type modules (e.g., `Ash.Type.String`) and simple atoms (e.g., `:string`). + + ## Parameters + + - `type` - An atom or module representing the field type + + ## Returns + + A human-readable string representation of the type + + ## Examples + + iex> format(:string) + "String" + + iex> format(Ash.Type.String) + "String" + + iex> format(Ash.Type.Date) + "Date" + """ + @spec format(atom() | module()) :: String.t() + def format(type) when is_atom(type) do + type_string = to_string(type) + + if String.contains?(type_string, "Ash.Type.") do + type_string + |> String.split(".") + |> List.last() + |> String.downcase() + |> then(fn type_name -> + try do + type_atom = String.to_existing_atom(type_name) + FieldTypes.label(type_atom) + rescue + ArgumentError -> FieldTypes.label(:string) + end + end) + else + FieldTypes.label(type) + end + end + + def format(type) do + to_string(type) + end +end From 4a1042ab1a9be026b412fb3d6f1df164fe08970b Mon Sep 17 00:00:00 2001 From: carla Date: Thu, 8 Jan 2026 11:28:27 +0100 Subject: [PATCH 14/23] feat: add atomic update for single member field visibility --- lib/membership/member.ex | 27 +-- lib/membership/setting.ex | 10 ++ .../update_single_member_field_visibility.ex | 164 ++++++++++++++++++ 3 files changed, 177 insertions(+), 24 deletions(-) create mode 100644 lib/membership/setting/changes/update_single_member_field_visibility.ex diff --git a/lib/membership/member.ex b/lib/membership/member.ex index 51da8ff..d2ea07d 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -40,6 +40,8 @@ defmodule Mv.Membership.Member do import Ash.Expr require Logger + alias Mv.Membership.Helpers.VisibilityConfig + # Module constants @member_search_limit 10 @@ -607,7 +609,7 @@ defmodule Mv.Membership.Member do {:ok, settings} -> visibility_config = settings.member_field_visibility || %{} # Normalize map keys to atoms (JSONB may return string keys) - normalized_config = normalize_visibility_config(visibility_config) + normalized_config = VisibilityConfig.normalize(visibility_config) # Get value from normalized config, use field-specific default Map.get(normalized_config, field, default_visibility) @@ -959,29 +961,6 @@ defmodule Mv.Membership.Member do defp error_type(error) when is_atom(error), do: error defp error_type(_), do: :unknown - # Normalizes visibility config map keys from strings to atoms. - # JSONB in PostgreSQL converts atom keys to string keys when storing. - defp normalize_visibility_config(config) when is_map(config) do - Enum.reduce(config, %{}, fn - {key, value}, acc when is_atom(key) -> - Map.put(acc, key, value) - - {key, value}, acc when is_binary(key) -> - try do - atom_key = String.to_existing_atom(key) - Map.put(acc, atom_key, value) - rescue - ArgumentError -> - acc - end - - _, acc -> - acc - end) - end - - defp normalize_visibility_config(_), do: %{} - @doc """ Performs fuzzy search on members using PostgreSQL trigram similarity. diff --git a/lib/membership/setting.ex b/lib/membership/setting.ex index eedc47c..4ba0794 100644 --- a/lib/membership/setting.ex +++ b/lib/membership/setting.ex @@ -91,6 +91,16 @@ defmodule Mv.Membership.Setting do accept [:member_field_visibility] end + update :update_single_member_field_visibility do + description "Atomically updates a single field in the member_field_visibility JSONB map" + require_atomic? false + + argument :field, :string, allow_nil?: false + argument :show_in_overview, :boolean, allow_nil?: false + + change Mv.Membership.Setting.Changes.UpdateSingleMemberFieldVisibility + end + update :update_membership_fee_settings do description "Updates the membership fee configuration" require_atomic? false diff --git a/lib/membership/setting/changes/update_single_member_field_visibility.ex b/lib/membership/setting/changes/update_single_member_field_visibility.ex new file mode 100644 index 0000000..e047cdf --- /dev/null +++ b/lib/membership/setting/changes/update_single_member_field_visibility.ex @@ -0,0 +1,164 @@ +defmodule Mv.Membership.Setting.Changes.UpdateSingleMemberFieldVisibility do + @moduledoc """ + Ash change that atomically updates a single field in the member_field_visibility JSONB map. + + This change uses PostgreSQL's jsonb_set function to atomically update a single key + in the JSONB map, preventing lost updates in concurrent scenarios. + + ## Arguments + - `field` - The member field name as a string (e.g., "street", "house_number") + - `show_in_overview` - Boolean value indicating visibility + + ## Example + settings + |> Ash.Changeset.for_update(:update_single_member_field_visibility, + %{}, + arguments: %{field: "street", show_in_overview: false} + ) + |> Ash.update(domain: Mv.Membership) + """ + use Ash.Resource.Change + + alias Ash.Error.Invalid + alias Ecto.Adapters.SQL + require Logger + + def change(changeset, _opts, _context) do + with {:ok, field} <- get_and_validate_field(changeset), + {:ok, show_in_overview} <- get_and_validate_boolean(changeset, :show_in_overview) do + add_after_action(changeset, field, show_in_overview) + else + {:error, updated_changeset} -> updated_changeset + end + end + + defp get_and_validate_field(changeset) do + case Ash.Changeset.get_argument(changeset, :field) do + nil -> + {:error, + add_error(changeset, + field: :member_field_visibility, + message: "field argument is required" + )} + + field -> + valid_fields = Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1) + + if field in valid_fields do + {:ok, field} + else + {:error, + add_error( + changeset, + field: :member_field_visibility, + message: "Invalid member field: #{field}" + )} + end + end + end + + defp get_and_validate_boolean(changeset, arg_name) do + case Ash.Changeset.get_argument(changeset, arg_name) do + nil -> + {:error, + add_error( + changeset, + field: :member_field_visibility, + message: "#{arg_name} argument is required" + )} + + value when is_boolean(value) -> + {:ok, value} + + _ -> + {:error, + add_error( + changeset, + field: :member_field_visibility, + message: "#{arg_name} must be a boolean" + )} + end + end + + defp add_error(changeset, opts) do + Ash.Changeset.add_error(changeset, opts) + end + + defp add_after_action(changeset, field, show_in_overview) do + # Use after_action to execute atomic SQL update + Ash.Changeset.after_action(changeset, fn _changeset, settings -> + # Use PostgreSQL jsonb_set for atomic update + # jsonb_set(target, path, new_value, create_missing?) + # path is an array: ['field_name'] + # new_value must be JSON: to_jsonb(boolean) + sql = """ + UPDATE settings + SET member_field_visibility = jsonb_set( + COALESCE(member_field_visibility, '{}'::jsonb), + ARRAY[$1::text], + to_jsonb($2::boolean), + true + ) + WHERE id = $3 + RETURNING member_field_visibility + """ + + # Convert UUID string to binary for PostgreSQL + uuid_binary = Ecto.UUID.dump!(settings.id) + + case SQL.query(Mv.Repo, sql, [field, show_in_overview, uuid_binary]) do + {:ok, %{rows: [[updated_jsonb] | _]}} -> + updated_visibility = normalize_jsonb_result(updated_jsonb) + + # Update the settings struct with the new visibility + updated_settings = %{settings | member_field_visibility: updated_visibility} + {:ok, updated_settings} + + {:ok, %{rows: []}} -> + {:error, + Invalid.exception( + field: :member_field_visibility, + message: "Settings not found" + )} + + {:error, error} -> + Logger.error("Failed to atomically update member_field_visibility: #{inspect(error)}") + + {:error, + Invalid.exception( + field: :member_field_visibility, + message: "Failed to update visibility" + )} + end + end) + end + + defp normalize_jsonb_result(updated_jsonb) do + case updated_jsonb do + map when is_map(map) -> + # Convert atom keys to strings if needed + Enum.reduce(map, %{}, fn + {k, v}, acc when is_atom(k) -> Map.put(acc, Atom.to_string(k), v) + {k, v}, acc -> Map.put(acc, k, v) + end) + + binary when is_binary(binary) -> + case Jason.decode(binary) do + {:ok, decoded} when is_map(decoded) -> + decoded + + # Not a map after decode + {:ok, _} -> + %{} + + {:error, reason} -> + Logger.warning("Failed to decode JSONB: #{inspect(reason)}") + %{} + end + + _ -> + Logger.warning("Unexpected JSONB format: #{inspect(updated_jsonb)}") + %{} + end + end +end From 30c43271eadfc9d23a9b22c6e7bf3ba7c8b97ce4 Mon Sep 17 00:00:00 2001 From: carla Date: Thu, 8 Jan 2026 11:37:07 +0100 Subject: [PATCH 15/23] refactor: remove code duplication using helper modules --- .../field_visibility_dropdown_component.ex | 8 +- .../live/member_field_live/form_component.ex | 163 +++--------------- .../live/member_field_live/index_component.ex | 69 +------- .../member_live/index/field_visibility.ex | 25 +-- 4 files changed, 45 insertions(+), 220 deletions(-) diff --git a/lib/mv_web/live/components/field_visibility_dropdown_component.ex b/lib/mv_web/live/components/field_visibility_dropdown_component.ex index 5fc0abf..426daed 100644 --- a/lib/mv_web/live/components/field_visibility_dropdown_component.ex +++ b/lib/mv_web/live/components/field_visibility_dropdown_component.ex @@ -18,6 +18,8 @@ defmodule MvWeb.Components.FieldVisibilityDropdownComponent do use MvWeb, :live_component + alias MvWeb.Translations.MemberFields + # --------------------------------------------------------------------------- # UPDATE # --------------------------------------------------------------------------- @@ -66,7 +68,7 @@ defmodule MvWeb.Components.FieldVisibilityDropdownComponent do <.dropdown_menu id="field-visibility-menu" icon="hero-adjustments-horizontal" - button_label={gettext("Columns")} + button_label={gettext("Show/Hide Columns")} items={@all_items} checkboxes={true} selected={@selected_fields} @@ -153,12 +155,12 @@ defmodule MvWeb.Components.FieldVisibilityDropdownComponent do defp field_to_string(field) when is_binary(field), do: field defp format_field_label(field) when is_atom(field) do - MvWeb.Translations.MemberFields.label(field) + MemberFields.label(field) end defp format_field_label(field) when is_binary(field) do case safe_to_existing_atom(field) do - {:ok, atom} -> MvWeb.Translations.MemberFields.label(atom) + {:ok, atom} -> MemberFields.label(atom) :error -> fallback_label(field) end end diff --git a/lib/mv_web/live/member_field_live/form_component.ex b/lib/mv_web/live/member_field_live/form_component.ex index 0f0b446..1bba048 100644 --- a/lib/mv_web/live/member_field_live/form_component.ex +++ b/lib/mv_web/live/member_field_live/form_component.ex @@ -3,22 +3,28 @@ defmodule MvWeb.MemberFieldLive.FormComponent do LiveComponent form for editing member field properties (embedded in settings). ## Features - - Edit member field properties (name, value type, description, immutable, required, show in overview) - - Display member field information from Member Resource + - Edit member field visibility (show_in_overview) + - Display member field information from Member Resource (read-only) - Restrict editing for email field (only show_in_overview can be changed) - Real-time validation - - Updates Settings.member_field_visibility + - Updates Settings.member_field_visibility atomically ## Props - `member_field` - The member field atom to edit (e.g., :first_name, :email) - `settings` - The current Settings resource - `on_save` - Callback function to call when form is saved - `on_cancel` - Callback function to call when form is cancelled + + ## Note + Member fields are technical fields that cannot be changed (name, value_type, description, required). + Only the visibility (show_in_overview) can be modified. """ use MvWeb, :live_component + alias Mv.Helpers.TypeParsers alias Mv.Membership - alias MvWeb.Translations.FieldTypes + alias Mv.Membership.Helpers.VisibilityConfig + alias MvWeb.Helpers.FieldTypeFormatter alias MvWeb.Translations.MemberFields @required_fields [:first_name, :last_name, :email] @@ -39,7 +45,7 @@ defmodule MvWeb.MemberFieldLive.FormComponent do type="button" phx-click="cancel" phx-target={@myself} - aria-label={gettext("Back to member field overview")} + aria-label={gettext("Back to Settings")} > <.icon name="hero-arrow-left" class="w-4 h-4" /> @@ -102,7 +108,7 @@ defmodule MvWeb.MemberFieldLive.FormComponent do type="text" name={@form[:value_type].name} id={@form[:value_type].id} - value={format_value_type(@field_attributes.value_type)} + value={FieldTypeFormatter.format(@field_attributes.value_type)} disabled readonly class="w-full input" @@ -148,47 +154,6 @@ defmodule MvWeb.MemberFieldLive.FormComponent do readonly={@is_email_field?} /> -
-
- -
-
- <.input - :if={not @is_email_field?} - field={@form[:immutable]} - type="checkbox" - label={gettext("Immutable")} - disabled={@is_email_field?} - readonly={@is_email_field?} - /> -
Map.put("show_in_overview", parse_boolean(member_field_params["show_in_overview"])) + |> Map.put( + "show_in_overview", + TypeParsers.parse_boolean(member_field_params["show_in_overview"]) + ) |> Map.put("name", form.source["name"]) |> Map.put("value_type", form.source["value_type"]) |> Map.put("description", form.source["description"]) - |> Map.put("immutable", form.source["immutable"]) |> Map.put("required", form.source["required"]) updated_form = @@ -284,29 +251,15 @@ defmodule MvWeb.MemberFieldLive.FormComponent do @impl true def handle_event("save", %{"member_field" => member_field_params}, socket) do # Only show_in_overview can be changed for member fields - show_in_overview = parse_boolean(member_field_params["show_in_overview"]) - - # Get current visibility config and update only the current field - current_visibility = socket.assigns.settings.member_field_visibility || %{} + show_in_overview = TypeParsers.parse_boolean(member_field_params["show_in_overview"]) field_string = Atom.to_string(socket.assigns.member_field) - # Normalize keys to strings - normalized_visibility = - Enum.reduce(current_visibility, %{}, fn - {key, value}, acc when is_atom(key) -> - Map.put(acc, Atom.to_string(key), value) - - {key, value}, acc when is_binary(key) -> - Map.put(acc, key, value) - end) - - # Update the specific field - updated_visibility = Map.put(normalized_visibility, field_string, show_in_overview) - - # Update settings with new visibility - case Membership.update_member_field_visibility( + # Use atomic action to update only this single field + # This prevents lost updates in concurrent scenarios + case Membership.update_single_member_field_visibility( socket.assigns.settings, - updated_visibility + field: field_string, + show_in_overview: show_in_overview ) do {:ok, _updated_settings} -> socket.assigns.on_save.(socket.assigns.member_field, "update") @@ -335,15 +288,15 @@ defmodule MvWeb.MemberFieldLive.FormComponent do defp assign_form(%{assigns: %{member_field: member_field, settings: settings}} = socket) do field_attributes = get_field_attributes(member_field) visibility_config = settings.member_field_visibility || %{} - normalized_config = normalize_visibility_config(visibility_config) + normalized_config = VisibilityConfig.normalize(visibility_config) show_in_overview = Map.get(normalized_config, member_field, true) # Create a manual form structure with string keys + # Note: immutable is not included as it's not editable for member fields form_data = %{ "name" => MemberFields.label(member_field), - "value_type" => format_value_type(field_attributes.value_type), + "value_type" => FieldTypeFormatter.format(field_attributes.value_type), "description" => field_attributes.description || "", - "immutable" => field_attributes.immutable, "required" => field_attributes.required, "show_in_overview" => show_in_overview } @@ -355,13 +308,14 @@ defmodule MvWeb.MemberFieldLive.FormComponent do defp get_field_attributes(field) when is_atom(field) do # Get attribute info from Member Resource - case Ash.Resource.Info.attribute(Mv.Membership.Member, field) do + alias Ash.Resource.Info + + case Info.attribute(Mv.Membership.Member, field) do nil -> # Fallback for fields not in resource (shouldn't happen with Constants) %{ value_type: :string, description: nil, - immutable: field == :email, required: field in @required_fields } @@ -369,72 +323,11 @@ defmodule MvWeb.MemberFieldLive.FormComponent do %{ value_type: attribute.type, description: nil, - immutable: field == :email, required: not attribute.allow_nil? } end end - defp format_value_type(type) when is_atom(type) do - type_string = to_string(type) - - # Check if it's an Ash type module (e.g., Ash.Type.String or Elixir.Ash.Type.String) - if String.contains?(type_string, "Ash.Type.") do - # Extract the base type name from Ash type modules - # e.g., "Elixir.Ash.Type.String" -> "String" -> :string - type_name = - type_string - |> String.split(".") - |> List.last() - |> String.downcase() - - try do - type_atom = String.to_existing_atom(type_name) - FieldTypes.label(type_atom) - rescue - ArgumentError -> - # Fallback if atom doesn't exist - FieldTypes.label(:string) - end - else - # It's already an atom like :string, :boolean, :date - FieldTypes.label(type) - end - end - - defp format_value_type(type) do - # Fallback for unknown types - to_string(type) - end - - defp normalize_visibility_config(config) when is_map(config) do - Enum.reduce(config, %{}, fn - {key, value}, acc when is_atom(key) -> - Map.put(acc, key, value) - - {key, value}, acc when is_binary(key) -> - try do - atom_key = String.to_existing_atom(key) - Map.put(acc, atom_key, value) - rescue - ArgumentError -> - acc - end - - _, acc -> - acc - end) - end - - defp normalize_visibility_config(_), do: %{} - - defp parse_boolean(value) when is_boolean(value), do: value - defp parse_boolean("true"), do: true - defp parse_boolean("false"), do: false - defp parse_boolean(1), do: true - defp parse_boolean(0), do: false - defp parse_boolean(_), do: false - defp format_error(%Ash.Error.Invalid{} = error) do Ash.ErrorKind.message(error) end diff --git a/lib/mv_web/live/member_field_live/index_component.ex b/lib/mv_web/live/member_field_live/index_component.ex index 2d4f1dc..5204030 100644 --- a/lib/mv_web/live/member_field_live/index_component.ex +++ b/lib/mv_web/live/member_field_live/index_component.ex @@ -11,8 +11,10 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do """ use MvWeb, :live_component + alias Ash.Resource.Info alias Mv.Membership - alias MvWeb.Translations.FieldTypes + alias Mv.Membership.Helpers.VisibilityConfig + alias MvWeb.Helpers.FieldTypeFormatter alias MvWeb.Translations.MemberFields @impl true @@ -180,11 +182,11 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do visibility_config = settings.member_field_visibility || %{} # Normalize visibility config keys to atoms - normalized_config = normalize_visibility_config(visibility_config) + normalized_config = VisibilityConfig.normalize(visibility_config) Enum.map(member_fields, fn field -> show_in_overview = Map.get(normalized_config, field, true) - attribute = Ash.Resource.Info.attribute(Mv.Membership.Member, field) + attribute = Info.attribute(Mv.Membership.Member, field) %{ field: field, @@ -199,68 +201,15 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do end defp format_value_type(field) when is_atom(field) do - case Ash.Resource.Info.attribute(Mv.Membership.Member, field) do - nil -> FieldTypes.label(:string) - attribute -> format_value_type(attribute.type) + case Info.attribute(Mv.Membership.Member, field) do + nil -> FieldTypeFormatter.format(:string) + attribute -> FieldTypeFormatter.format(attribute.type) end end - defp format_value_type(type) when is_atom(type) do - type_string = to_string(type) - - # Check if it's an Ash type module (e.g., Ash.Type.String or Elixir.Ash.Type.String) - if String.contains?(type_string, "Ash.Type.") do - # Extract the base type name from Ash type modules - # e.g., "Elixir.Ash.Type.String" -> "String" -> :string - type_name = - type_string - |> String.split(".") - |> List.last() - |> String.downcase() - - try do - type_atom = String.to_existing_atom(type_name) - FieldTypes.label(type_atom) - rescue - ArgumentError -> - # Fallback if atom doesn't exist - FieldTypes.label(:string) - end - else - # It's already an atom like :string, :boolean, :date - FieldTypes.label(type) - end - end - - defp format_value_type(type) do - # Fallback for unknown types - to_string(type) - end - - defp normalize_visibility_config(config) when is_map(config) do - Enum.reduce(config, %{}, fn - {key, value}, acc when is_atom(key) -> - Map.put(acc, key, value) - - {key, value}, acc when is_binary(key) -> - try do - atom_key = String.to_existing_atom(key) - Map.put(acc, atom_key, value) - rescue - ArgumentError -> - acc - end - - _, acc -> - acc - end) - end - - defp normalize_visibility_config(_), do: %{} - # Check if a field is required by checking the actual attribute definition defp required?(field) when is_atom(field) do - case Ash.Resource.Info.attribute(Mv.Membership.Member, field) do + case Info.attribute(Mv.Membership.Member, field) do nil -> false attribute -> not attribute.allow_nil? end diff --git a/lib/mv_web/live/member_live/index/field_visibility.ex b/lib/mv_web/live/member_live/index/field_visibility.ex index 627bbcf..9ba9267 100644 --- a/lib/mv_web/live/member_live/index/field_visibility.ex +++ b/lib/mv_web/live/member_live/index/field_visibility.ex @@ -20,6 +20,8 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do 3. Default (all fields visible) """ + alias Mv.Membership.Helpers.VisibilityConfig + @doc """ Gets all available fields for selection. @@ -177,7 +179,7 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do # Gets member field visibility from settings defp get_member_field_visibility_from_settings(settings) do visibility_config = - normalize_visibility_config(Map.get(settings, :member_field_visibility, %{})) + VisibilityConfig.normalize(Map.get(settings, :member_field_visibility, %{})) member_fields = Mv.Constants.member_fields() @@ -201,27 +203,6 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do end) end - # Normalizes visibility config map keys from strings to atoms - defp normalize_visibility_config(config) when is_map(config) do - Enum.reduce(config, %{}, fn - {key, value}, acc when is_atom(key) -> - Map.put(acc, key, value) - - {key, value}, acc when is_binary(key) -> - try do - atom_key = String.to_existing_atom(key) - Map.put(acc, atom_key, value) - rescue - ArgumentError -> acc - end - - _, acc -> - acc - end) - end - - defp normalize_visibility_config(_), do: %{} - # Converts field string to atom (for member fields) or keeps as string (for custom fields) defp to_field_identifier(field_string) when is_binary(field_string) do if String.starts_with?(field_string, Mv.Constants.custom_field_prefix()) do From b139d857914ab5f5528a683b4549fc1ed9755e21 Mon Sep 17 00:00:00 2001 From: carla Date: Thu, 8 Jan 2026 11:37:39 +0100 Subject: [PATCH 16/23] fix: add missing event handler for member field visibility updates --- lib/mv_web/live/global_settings_live.ex | 40 ++++++++----------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/lib/mv_web/live/global_settings_live.ex b/lib/mv_web/live/global_settings_live.ex index 2798412..3696880 100644 --- a/lib/mv_web/live/global_settings_live.ex +++ b/lib/mv_web/live/global_settings_live.ex @@ -141,33 +141,6 @@ defmodule MvWeb.GlobalSettingsLive do {:noreply, put_flash(socket, :error, gettext("Slug does not match. Deletion cancelled."))} end - @impl true - def handle_info({:member_field_visibility_updated}, socket) do - # Reload settings to get updated member_field_visibility - {:ok, updated_settings} = Membership.get_settings() - - {:noreply, - socket - |> assign(:settings, updated_settings) - |> put_flash(:info, gettext("Member field visibility updated successfully"))} - end - - @impl true - def handle_info({:member_field_visibility_error, error}, socket) do - error_message = - case error do - %Ash.Error.Invalid{} = invalid_error -> - gettext("Failed to update member field visibility: %{error}", - error: Ash.ErrorKind.message(invalid_error) - ) - - error -> - gettext("Failed to update member field visibility: %{error}", error: inspect(error)) - end - - {:noreply, put_flash(socket, :error, error_message)} - end - @impl true def handle_info({:editing_section_changed, section}, socket) do {:noreply, assign(socket, :active_editing_section, section)} @@ -192,6 +165,19 @@ defmodule MvWeb.GlobalSettingsLive do |> put_flash(:info, gettext("Member field %{action} successfully", action: action))} end + @impl true + def handle_info({:member_field_visibility_updated}, socket) do + # Legacy event - reload settings and update component + {:ok, updated_settings} = Membership.get_settings() + + send_update(MvWeb.MemberFieldLive.IndexComponent, + id: "member-fields-component", + settings: updated_settings + ) + + {:noreply, assign(socket, :settings, updated_settings)} + end + defp assign_form(%{assigns: %{settings: settings}} = socket) do form = AshPhoenix.Form.for_update( From e565d1748e3dd4470032310f257609f0bbde5ac0 Mon Sep 17 00:00:00 2001 From: carla Date: Thu, 8 Jan 2026 11:38:41 +0100 Subject: [PATCH 17/23] test: add tests for atomic member field visibility updates --- .../member_field_visibility_test.exs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/test/membership/member_field_visibility_test.exs b/test/membership/member_field_visibility_test.exs index 6bc04f6..47ab5bd 100644 --- a/test/membership/member_field_visibility_test.exs +++ b/test/membership/member_field_visibility_test.exs @@ -80,4 +80,72 @@ defmodule Mv.Membership.MemberFieldVisibilityTest do end) end end + + describe "update_single_member_field_visibility/3" do + test "atomically updates a single field in member_field_visibility" do + {:ok, settings} = Mv.Membership.get_settings() + field_string = "street" + + # Update single field + {:ok, updated_settings} = + Mv.Membership.update_single_member_field_visibility( + settings, + field: field_string, + show_in_overview: false + ) + + # Verify the field was updated + assert updated_settings.member_field_visibility[field_string] == false + + # Verify other fields are not affected + other_fields = + Mv.Constants.member_fields() + |> Enum.reject(&(&1 == String.to_existing_atom(field_string))) + + Enum.each(other_fields, fn field -> + field_string = Atom.to_string(field) + # Fields not explicitly set should default to true (except exit_date) + expected = if field == :exit_date, do: false, else: true + + assert Map.get(updated_settings.member_field_visibility, field_string, expected) == + expected + end) + end + + test "returns error for invalid field name" do + {:ok, settings} = Mv.Membership.get_settings() + + assert {:error, %Ash.Error.Invalid{errors: [%{field: :member_field_visibility}]}} = + Mv.Membership.update_single_member_field_visibility( + settings, + field: "invalid_field", + show_in_overview: false + ) + end + + test "handles concurrent updates atomically" do + {:ok, settings} = Mv.Membership.get_settings() + field1 = "street" + field2 = "house_number" + + # Simulate concurrent updates by updating different fields + {:ok, updated1} = + Mv.Membership.update_single_member_field_visibility( + settings, + field: field1, + show_in_overview: false + ) + + {:ok, updated2} = + Mv.Membership.update_single_member_field_visibility( + updated1, + field: field2, + show_in_overview: true + ) + + # Both fields should be correctly updated + assert updated2.member_field_visibility[field1] == false + assert updated2.member_field_visibility[field2] == true + end + end end From 0ccb1c7d7979fb3369997f586551ec0f6b0e98db Mon Sep 17 00:00:00 2001 From: carla Date: Thu, 8 Jan 2026 11:39:16 +0100 Subject: [PATCH 18/23] fix: add label for membership fee type --- lib/mv_web/translations/member_fields.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/mv_web/translations/member_fields.ex b/lib/mv_web/translations/member_fields.ex index 2d6834a..26f55ac 100644 --- a/lib/mv_web/translations/member_fields.ex +++ b/lib/mv_web/translations/member_fields.ex @@ -27,6 +27,7 @@ defmodule MvWeb.Translations.MemberFields do def label(:street), do: gettext("Street") def label(:house_number), do: gettext("House Number") def label(:postal_code), do: gettext("Postal Code") + def label(:membership_fee_start_date), do: gettext("Membership Fee Start Date") # Fallback for unknown fields def label(field) do From 47c46eaebfaca33f221c81ce84174c9636f84170 Mon Sep 17 00:00:00 2001 From: carla Date: Thu, 8 Jan 2026 11:40:04 +0100 Subject: [PATCH 19/23] i18n: update translations --- .../live/contribution_type_live/index.ex | 2 +- .../show/membership_fees_component.ex | 10 +- .../live/membership_fee_type_live/index.ex | 2 +- priv/gettext/de/LC_MESSAGES/default.po | 807 +++++++++--------- priv/gettext/default.pot | 569 ++++++++++-- priv/gettext/en/LC_MESSAGES/default.po | 477 ++++++----- 6 files changed, 1204 insertions(+), 663 deletions(-) diff --git a/lib/mv_web/live/contribution_type_live/index.ex b/lib/mv_web/live/contribution_type_live/index.ex index 9a7b602..3e2f04c 100644 --- a/lib/mv_web/live/contribution_type_live/index.ex +++ b/lib/mv_web/live/contribution_type_live/index.ex @@ -115,7 +115,7 @@ defmodule MvWeb.ContributionTypeLive.Index do

{gettext( - "Contribution types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation." + "Contribution types define different membership fee structures. Each type has a fixed cycle (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation." )}

    diff --git a/lib/mv_web/live/member_live/show/membership_fees_component.ex b/lib/mv_web/live/member_live/show/membership_fees_component.ex index 0bc93a1..d8c49eb 100644 --- a/lib/mv_web/live/member_live/show/membership_fees_component.ex +++ b/lib/mv_web/live/member_live/show/membership_fees_component.ex @@ -63,7 +63,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do phx-click="delete_all_cycles" phx-target={@myself} class="btn btn-sm btn-error btn-outline" - title={gettext("Delete all cycles")} + title={gettext("Delete All Cycles")} > <.icon name="hero-trash" class="size-4" /> {gettext("Delete All Cycles")} @@ -168,7 +168,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do phx-value-cycle_id={cycle.id} phx-target={@myself} class="btn btn-sm btn-error btn-outline" - title={gettext("Delete cycle")} + title={gettext("Delete Cycle")} > <.icon name="hero-trash" class="size-4" /> {gettext("Delete")} @@ -329,16 +329,14 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do />
<%= if @create_cycle_date do %>
{format_create_cycle_period( diff --git a/lib/mv_web/live/membership_fee_type_live/index.ex b/lib/mv_web/live/membership_fee_type_live/index.ex index 262983f..f105058 100644 --- a/lib/mv_web/live/membership_fee_type_live/index.ex +++ b/lib/mv_web/live/membership_fee_type_live/index.ex @@ -115,7 +115,7 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do phx-value-id={mft.id} data-confirm={gettext("Are you sure?")} class="btn btn-ghost btn-xs text-error" - aria-label={gettext("Delete membership fee type")} + aria-label={gettext("Delete Membership Fee Type")} > <.icon name="hero-trash" class="size-4" /> diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 2947204..2bba1cd 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -122,6 +122,7 @@ msgid "close" msgstr "schließen" #: lib/mv_web/live/member_live/form.ex +#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/show.ex #: lib/mv_web/translations/member_fields.ex #, elixir-autogen, elixir-format @@ -183,7 +184,6 @@ msgstr "Straße" #: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/member_field_live/index_component.ex -#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index/formatter.ex #: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format @@ -197,7 +197,6 @@ msgstr "Mitglied anzeigen" #: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/member_field_live/index_component.ex -#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index/formatter.ex #: lib/mv_web/live/member_live/show.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex @@ -290,12 +289,6 @@ msgstr "Benutzer*in bearbeiten" msgid "Enabled" msgstr "Aktiviert" -#: lib/mv_web/live/custom_field_live/form_component.ex -#: lib/mv_web/live/member_field_live/form_component.ex -#, elixir-autogen, elixir-format -msgid "Immutable" -msgstr "Unveränderlich" - #: lib/mv_web/components/layouts/navbar.ex #, elixir-autogen, elixir-format msgid "Logout" @@ -630,7 +623,6 @@ msgstr "Benutzerdefinierter Feldwert erfolgreich %{action}" msgid "Please select a custom field first" msgstr "Bitte wähle zuerst ein Benutzerdefiniertes Feld" -#: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/member_live/form.ex #: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format @@ -893,6 +885,7 @@ msgid "Amount" msgstr "Betrag" #: lib/mv_web/live/contribution_period_live/show.ex +#: lib/mv_web/live/member_field_live/form_component.ex #, elixir-autogen, elixir-format msgid "Back to Settings" msgstr "Zurück zu den Einstellungen" @@ -928,11 +921,6 @@ msgstr "Beitragsarten" msgid "Contribution type" msgstr "Beitragsart" -#: lib/mv_web/live/contribution_type_live/index.ex -#, elixir-autogen, elixir-format -msgid "Contribution types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation." -msgstr "Beitragsarten definieren verschiedene Beitragsmodelle. Jede Art hat einen festen Zyklus (monatlich, vierteljährlich, halbjährlich, jährlich), der nach Erstellung nicht mehr geändert werden kann." - #: lib/mv_web/components/layouts/navbar.ex #, elixir-autogen, elixir-format, fuzzy msgid "Contributions" @@ -1002,7 +990,7 @@ msgstr "Ehrenamtlich" #: lib/mv_web/live/membership_fee_type_live/index.ex #, elixir-autogen, elixir-format msgid "Interval" -msgstr "Zyklus" +msgstr "Intervall" #: lib/mv_web/live/membership_fee_settings_live.ex #, elixir-autogen, elixir-format, fuzzy @@ -1230,11 +1218,6 @@ msgstr "Warum werden nicht alle Beitragsarten angezeigt?" msgid "Yearly" msgstr "jährlich" -#: lib/mv_web/live/components/field_visibility_dropdown_component.ex -#, elixir-autogen, elixir-format -msgid "Columns" -msgstr "Spalten" - #: lib/mv_web/live/components/field_visibility_dropdown_component.ex #, elixir-autogen, elixir-format msgid "Custom Field %{id}" @@ -1279,7 +1262,7 @@ msgstr "Benutzerdefiniertes Feld erfolgreich gelöscht" #: lib/mv_web/live/custom_field_live/form_component.ex #, elixir-autogen, elixir-format, fuzzy msgid "Edit Custom Field" -msgstr "Benutzerdefiniertes Feld löschen" +msgstr "Benutzerdefiniertes Feld bearbeiten" #: lib/mv_web/live/global_settings_live.ex #, elixir-autogen, elixir-format @@ -1308,8 +1291,6 @@ msgstr "Diese Felder können zusätzlich zu den normalen Daten ausgefüllt werde msgid "Value Type" msgstr "Wertetyp" -#: lib/mv_web/live/member_field_live/form_component.ex -#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex #: lib/mv_web/translations/field_types.ex #, elixir-autogen, elixir-format @@ -1337,16 +1318,6 @@ msgid "Yes/No-Selection" msgstr "Ja/Nein-Auswahl" #: lib/mv_web/live/global_settings_live.ex -#, elixir-autogen, elixir-format -msgid "Failed to update member field visibility: %{error}" -msgstr "Fehler beim anpassen der Sichtbarkeit des Feldes: %{error}" - -#: lib/mv_web/live/global_settings_live.ex -#, elixir-autogen, elixir-format -msgid "Member field visibility updated successfully" -msgstr "Sichtbarkeit des Feldes erfolgreich aktualisiert." - -#: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format, fuzzy msgid "Memberdata" msgstr "Mitgliederdaten" @@ -1362,139 +1333,26 @@ msgstr "Optional" msgid "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview." msgstr "Diese Datenfelder sind für MILA notwendig um Mitglieder zu identifizieren und zukünftig Beitragszahlungen zu berechnen. Aus diesem Grund können sie nicht gelöscht, aber in der Übersicht ausgeblendet werden." -#: lib/mv_web/live/member_field_live/form_component.ex -#, elixir-autogen, elixir-format -msgid "Back to member field overview" -msgstr "" - -#: lib/mv_web/live/member_field_live/form_component.ex -#: lib/mv_web/live/member_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "Boolean" -msgstr "" - -#: lib/mv_web/live/member_field_live/form_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Edit Member Field: %{field}" -msgstr "Mitglied bearbeiten" - #: lib/mv_web/live/global_settings_live.ex #, elixir-autogen, elixir-format, fuzzy msgid "Member field %{action} successfully" msgstr "Mitglied wurde erfolgreich %{action}" -#: lib/mv_web/live/member_field_live/form_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Save Member Field" -msgstr "Mitglied speichern" - -#: lib/mv_web/live/member_field_live/form_component.ex -#: lib/mv_web/live/member_field_live/index_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "String" -msgstr "Einstellungen" - -#: lib/mv_web/live/member_live/index.html.heex -#, elixir-autogen, elixir-format, fuzzy -msgid "Copy email addresses" -msgstr "E-Mail-Adressen kopieren" - -#: lib/mv_web/live/custom_field_live/form_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Save Custom Field" -msgstr "Benutzerdefiniertes Feld speichern" - -#: lib/mv_web/live/custom_field_value_live/form.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Save Custom Field Value" -msgstr "Benutzerdefinierten Feldwert speichern" - -#: lib/mv_web/components/core_components.ex +#: lib/mv_web/live/member_live/show/membership_fees_component.ex #, elixir-autogen, elixir-format -msgid "This field is required" -msgstr "Dieses Feld ist erforderlich" - -#: lib/mv_web/live/membership_fee_settings_live.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Configure global settings for membership fees." -msgstr "Globale Einstellungen für Mitgliedsbeiträge konfigurieren." - -#: lib/mv_web/live/membership_fee_settings_live.ex -#, elixir-autogen, elixir-format -msgid "Default Membership Fee Type" -msgstr "Standard-Mitgliedsbeitragsart" - -#: lib/mv_web/live/membership_fee_settings_live.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Generated cycles" -msgstr "Generierte Zyklen" - -#: lib/mv_web/live/membership_fee_settings_live.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Include joining cycle" -msgstr "Beitrittsdatum einbeziehen" - -#: lib/mv_web/components/layouts/navbar.ex -#: lib/mv_web/live/membership_fee_settings_live.ex -#, elixir-autogen, elixir-format -msgid "Membership Fee Settings" -msgstr "Mitgliedsbeitragseinstellungen" - -#: lib/mv_web/live/membership_fee_settings_live.ex -#, elixir-autogen, elixir-format -msgid "Membership fee start" -msgstr "Beitragsbeginn" - -#: lib/mv_web/live/membership_fee_settings_live.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Monthly Interval - Joining Cycle Included" -msgstr "Monatliches Intervall – Beitrittszeitraum einbezogen" - -#: lib/mv_web/live/membership_fee_settings_live.ex -#, elixir-autogen, elixir-format -msgid "None (no default)" -msgstr "Keine (kein Standard)" - -#: lib/mv_web/live/membership_fee_settings_live.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Quarterly Interval - Joining Cycle Excluded" -msgstr "Vierteljährliches Intervall – Beitrittszeitraum nicht einbezogen" - -#: lib/mv_web/live/membership_fee_settings_live.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Settings saved successfully." -msgstr "Einstellungen erfolgreich gespeichert" - -#: lib/mv_web/live/membership_fee_settings_live.ex -#, elixir-autogen, elixir-format -msgid "This membership fee type is automatically assigned to all new members. Can be changed individually per member." -msgstr "Diese Mitgliedsbeitragsart wird automatisch allen neuen Mitgliedern zugewiesen. Kann individuell pro Mitglied geändert werden." - -#: lib/mv_web/live/membership_fee_settings_live.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "When active: Members pay from the cycle of their joining." -msgstr "Wenn aktiviert: Mitglieder zahlen ab dem Zeitraum ihres Beitritts." - -#: lib/mv_web/live/membership_fee_settings_live.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "When inactive: Members pay from the next full cycle after joining." -msgstr "Wenn deaktiviert: Mitglieder zahlen ab dem nächsten vollen Beitragszyklus nach dem Beitritt." - -#: lib/mv_web/live/membership_fee_settings_live.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Yearly Interval - Joining Cycle Excluded" -msgstr "Jährliches Intervall – Beitrittszeitraum nicht einbezogen" - -#: lib/mv_web/live/membership_fee_settings_live.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Yearly Interval - Joining Cycle Included" -msgstr "Jährliches Intervall – Beitrittszeitraum einbezogen" +msgid "A cycle for this period already exists" +msgstr "Für dieses Intervall besteht bereits ein Zyklus." #: lib/mv_web/live/membership_fee_type_live/index.ex #, elixir-autogen, elixir-format msgid "About Membership Fee Types" msgstr "Über Mitgliedsbeitragsarten" +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "All cycles deleted" +msgstr "Alle Zyklen gelöscht" + #: lib/mv_web/live/membership_fee_type_live/form.ex #, elixir-autogen, elixir-format msgid "Already paid cycles will remain with the old amount." @@ -1526,16 +1384,56 @@ msgstr "Betrag ändern?" msgid "Changing the amount will affect %{count} member(s)." msgstr "Die Änderung des Betrags betrifft %{count} Mitglied(er)." +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Click to edit amount" +msgstr "Klicke um den Betrag zu ändern" + +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Configure global settings for membership fees." +msgstr "Globale Einstellungen für Mitgliedsbeiträge konfigurieren." + #: lib/mv_web/live/membership_fee_type_live/form.ex #, elixir-autogen, elixir-format msgid "Confirm Change" msgstr "Änderung bestätigen" +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Confirmation text does not match" +msgstr "Bestätigungstext stimmt nicht überein" + +#: lib/mv_web/live/member_live/index.html.heex +#, elixir-autogen, elixir-format, fuzzy +msgid "Copy email addresses" +msgstr "E-Mail-Adressen kopieren" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Create" +msgstr "erstellt" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Create Cycle" +msgstr "Aktueller Zyklus" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Create a new cycle manually" +msgstr "Erstelle manuell einen neuen Zyklus" + #: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "Current Cycle" msgstr "Aktueller Zyklus" +#: lib/mv_web/live/member_live/index.html.heex +#, elixir-autogen, elixir-format +msgid "Current Cycle Payment Status" +msgstr "Aktueller Zyklus Zahlungsstatus" + #: lib/mv_web/live/membership_fee_type_live/form.ex #, elixir-autogen, elixir-format msgid "Current amount" @@ -1551,6 +1449,11 @@ msgstr "Zyklus" msgid "Cycle amount updated" msgstr "Zyklusbetrag aktualisiert" +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Cycle created successfully" +msgstr "Zyklen erfolgreich regeneriert" + #: lib/mv_web/live/member_live/show/membership_fees_component.ex #, elixir-autogen, elixir-format msgid "Cycle deleted" @@ -1566,6 +1469,21 @@ msgstr "Zyklenstatus aktualisiert" msgid "Cycles regenerated successfully" msgstr "Zyklen erfolgreich regeneriert" +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Default Membership Fee Type" +msgstr "Standard-Mitgliedsbeitragsart" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Delete All" +msgstr "Löschen" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Delete All Cycles" +msgstr "Alle Zyklen löschen" + #: lib/mv_web/live/member_live/show/membership_fees_component.ex #, elixir-autogen, elixir-format, fuzzy msgid "Delete Cycle" @@ -1576,11 +1494,21 @@ msgstr "Zyklus löschen" msgid "Edit Cycle Amount" msgstr "Zyklusbetrag bearbeiten" +#: lib/mv_web/live/member_field_live/form_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Edit Field: %{field}" +msgstr "Mitglied bearbeiten" + #: lib/mv_web/live/membership_fee_type_live/form.ex #, elixir-autogen, elixir-format msgid "Edit Membership Fee Type" msgstr "Mitgliedsbeitragsart bearbeiten" +#: lib/mv_web/live/membership_fee_type_live/index.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Edit membership fee type" +msgstr "Mitgliedsbeitragsart bearbeiten" + #: lib/mv_web/live/member_live/show/membership_fees_component.ex #, elixir-autogen, elixir-format msgid "Failed to update cycle status: %{errors}" @@ -1596,6 +1524,16 @@ msgstr "Zukünftige unbezahlte Zyklen werden mit dem neuen Betrag regeneriert." msgid "Generate cycles from the last existing cycle to today" msgstr "Zyklen vom letzten existierenden Zyklus bis heute generieren" +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Generated cycles" +msgstr "Generierte Zyklen" + +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Include joining cycle" +msgstr "Beitrittsdatum einbeziehen" + #: lib/mv_web/live/membership_fee_type_live/form.ex #, elixir-autogen, elixir-format msgid "Interval cannot be changed after creation." @@ -1606,11 +1544,21 @@ msgstr "Das Intervall kann nach der Erstellung nicht geändert werden." msgid "Invalid amount format" msgstr "Ungültiges Betragsformat" +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Invalid date format" +msgstr "Ungültiges Betragsformat" + #: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "Last Cycle" msgstr "Letzter Zyklus" +#: lib/mv_web/live/member_live/index.html.heex +#, elixir-autogen, elixir-format +msgid "Last Cycle Payment Status" +msgstr "Letzter Zyklus Zahlungsstatus" + #: lib/mv_web/live/membership_fee_type_live/index.ex #, elixir-autogen, elixir-format msgid "Manage membership fee types for membership fees." @@ -1637,6 +1585,12 @@ msgstr "Als unbezahlt markieren" msgid "Membership Fee" msgstr "Mitgliedsbeitrag" +#: lib/mv_web/components/layouts/navbar.ex +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Membership Fee Settings" +msgstr "Mitgliedsbeitragseinstellungen" + #: lib/mv_web/live/member_live/index.html.heex #, elixir-autogen, elixir-format, fuzzy msgid "Membership Fee Status" @@ -1660,6 +1614,11 @@ msgstr "Mitgliedsbeitragsarten" msgid "Membership Fees" msgstr "Mitgliedsbeiträge" +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Membership fee start" +msgstr "Beitragsbeginn" + #: lib/mv_web/live/membership_fee_type_live/index.ex #, elixir-autogen, elixir-format msgid "Membership fee type deleted" @@ -1685,6 +1644,11 @@ msgstr "Mitgliedsbeitragsart aktualisiert. Zyklen regeneriert." msgid "Membership fee types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation." msgstr "Mitgliedsbeitragsarten definieren verschiedene Mitgliedsbeitragsstrukturen. Jede Art hat ein festes Intervall (monatlich, vierteljährlich, halbjährlich, jährlich), das nach der Erstellung nicht geändert werden kann." +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Monthly Interval - Joining Cycle Included" +msgstr "Monatliches Intervall – Beitrittszeitraum einbezogen" + #: lib/mv_web/live/membership_fee_type_live/form.ex #: lib/mv_web/live/membership_fee_type_live/index.ex #, elixir-autogen, elixir-format @@ -1706,6 +1670,11 @@ msgstr "Kein Zyklus" msgid "No cycles" msgstr "Keine Zyklen" +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "No cycles to delete" +msgstr "Keine Zyklen" + #: lib/mv_web/live/member_live/show/membership_fees_component.ex #, elixir-autogen, elixir-format msgid "No membership fee cycles found. Cycles will be generated automatically when a membership fee type is assigned." @@ -1722,11 +1691,31 @@ msgstr "Keine Mitgliedsbeitragsart zugewiesen" msgid "No status" msgstr "Kein Status" +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "None (no default)" +msgstr "Keine (kein Standard)" + +#: lib/mv_web/live/member_live/show.ex +#, elixir-autogen, elixir-format +msgid "Not set" +msgstr "Nicht gesetzt" + +#: lib/mv_web/live/member_live/show.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Payment Interval" +msgstr "Zahlungsfilter" + #: lib/mv_web/live/membership_fee_type_live/form.ex #, elixir-autogen, elixir-format msgid "Please confirm the amount change first" msgstr "Bitte bestätigen Sie zuerst die Betragsänderung" +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Quarterly Interval - Joining Cycle Excluded" +msgstr "Vierteljährliches Intervall – Beitrittszeitraum nicht einbezogen" + #: lib/mv_web/live/member_live/show/membership_fees_component.ex #, elixir-autogen, elixir-format msgid "Regenerate Cycles" @@ -1737,6 +1726,21 @@ msgstr "Zyklen regenerieren" msgid "Regenerating..." msgstr "Regeneriere..." +#: lib/mv_web/live/custom_field_live/form_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Save Custom Field" +msgstr "Benutzerdefiniertes Feld speichern" + +#: lib/mv_web/live/custom_field_value_live/form.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Save Custom Field Value" +msgstr "Benutzerdefinierten Feldwert speichern" + +#: lib/mv_web/live/member_field_live/form_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Save Field" +msgstr "Benutzerdefiniertes Feld speichern" + #: lib/mv_web/live/membership_fee_type_live/form.ex #, elixir-autogen, elixir-format msgid "Save Membership Fee Type" @@ -1752,270 +1756,106 @@ msgstr "Wählen Sie eine Mitgliedsbeitragsart für dieses Mitglied. Mitglieder k msgid "Select interval" msgstr "Intervall auswählen" +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Settings saved successfully." +msgstr "Einstellungen erfolgreich gespeichert" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "This action cannot be undone." +msgstr "Diese Aktion kann nicht rückgängig gemacht werden." + +#: lib/mv_web/components/core_components.ex +#, elixir-autogen, elixir-format +msgid "This field is required" +msgstr "Dieses Feld ist erforderlich" + +#: lib/mv_web/live/member_field_live/form_component.ex +#, elixir-autogen, elixir-format +msgid "This is a technical field and cannot be changed" +msgstr "Dies ist ein technisches Feld und kann nicht verändert werden." + +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "This membership fee type is automatically assigned to all new members. Can be changed individually per member." +msgstr "Diese Mitgliedsbeitragsart wird automatisch allen neuen Mitgliedern zugewiesen. Kann individuell pro Mitglied geändert werden." + #: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "Type" msgstr "Art" +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Type '%{confirmation}' to confirm" +msgstr "Trage '%{confirmation}' ein um zu bestätigen" + #: lib/mv_web/live/membership_fee_type_live/form.ex #, elixir-autogen, elixir-format msgid "Use this form to manage membership fee types in your database." msgstr "Verwenden Sie dieses Formular, um Mitgliedsbeitragsarten in Ihrer Datenbank zu verwalten." +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Warning" +msgstr "Warnung" + #: lib/mv_web/live/member_live/form.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex #, elixir-autogen, elixir-format msgid "Warning: Changing from %{old_interval} to %{new_interval} is not allowed. Please select a membership fee type with the same interval." msgstr "Warnung: Wechsel von %{old_interval} zu %{new_interval} ist nicht erlaubt. Bitte wählen Sie eine Mitgliedsbeitragsart mit demselben Intervall." -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format -msgid "A cycle for this period already exists" -msgstr "" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format -msgid "All cycles deleted" -msgstr "" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format -msgid "Click to edit amount" -msgstr "" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#: lib/mv_web/live/membership_fee_settings_live.ex #, elixir-autogen, elixir-format, fuzzy -msgid "Create" -msgstr "erstellt" +msgid "When active: Members pay from the cycle of their joining." +msgstr "Wenn aktiviert: Mitglieder zahlen ab dem Zeitraum ihres Beitritts." -#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#: lib/mv_web/live/membership_fee_settings_live.ex #, elixir-autogen, elixir-format, fuzzy -msgid "Create Cycle" -msgstr "Aktueller Zyklus" +msgid "When inactive: Members pay from the next full cycle after joining." +msgstr "Wenn deaktiviert: Mitglieder zahlen ab dem nächsten vollen Beitragszyklus nach dem Beitritt." -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format -msgid "Create a new cycle manually" -msgstr "" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#: lib/mv_web/live/membership_fee_settings_live.ex #, elixir-autogen, elixir-format, fuzzy -msgid "Cycle Period" -msgstr "Zyklus" +msgid "Yearly Interval - Joining Cycle Excluded" +msgstr "Jährliches Intervall – Beitrittszeitraum nicht einbezogen" -#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#: lib/mv_web/live/membership_fee_settings_live.ex #, elixir-autogen, elixir-format, fuzzy -msgid "Cycle created successfully" -msgstr "Zyklen erfolgreich regeneriert" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Delete All" -msgstr "Löschen" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Delete All Cycles" -msgstr "Alle Zyklen löschen" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Delete all cycles" -msgstr "Zyklus löschen" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Delete cycle" -msgstr "Zyklus löschen" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Invalid date format" -msgstr "Ungültiges Betragsformat" - -#: lib/mv_web/live/member_live/show.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Payment Interval" -msgstr "Zahlungsfilter" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format -msgid "The cycle period will be calculated based on this date and the interval." -msgstr "" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format -msgid "This action cannot be undone." -msgstr "" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format -msgid "Type '%{confirmation}' to confirm" -msgstr "" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format -msgid "Warning" -msgstr "" +msgid "Yearly Interval - Joining Cycle Included" +msgstr "Jährliches Intervall – Beitrittszeitraum einbezogen" #: lib/mv_web/live/member_live/show/membership_fees_component.ex #, elixir-autogen, elixir-format msgid "You are about to delete all %{count} cycles for this member." -msgstr "" +msgstr "Du bist dabei alle %{count} Zyklen für dieses Mitglied zu löschen." -#: lib/mv_web/live/member_live/index.html.heex -#, elixir-autogen, elixir-format -msgid "Current Cycle Payment Status" -msgstr "Aktueller Zyklus Zahlungsstatus" - -#: lib/mv_web/live/member_live/index.html.heex -#, elixir-autogen, elixir-format -msgid "Last Cycle Payment Status" -msgstr "Letzter Zyklus Zahlungsstatus" - -#: lib/mv_web/live/membership_fee_type_live/index.ex -#, elixir-autogen, elixir-format -msgid "Delete membership fee type" -msgstr "" +#: lib/mv_web/live/contribution_type_live/index.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Contribution types define different membership fee structures. Each type has a fixed cycle (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation." +msgstr "Beitragsarten definieren verschiedene Beitragsmodelle. Jede Art hat einen festen Zyklus (monatlich, vierteljährlich, halbjährlich, jährlich), der nach Erstellung nicht mehr geändert werden kann." #: lib/mv_web/live/membership_fee_type_live/index.ex #, elixir-autogen, elixir-format, fuzzy -msgid "Edit membership fee type" -msgstr "Mitgliedsbeitragsart bearbeiten" +msgid "Delete Membership Fee Type" +msgstr "Mitgliedsbeitragsart löschen" -#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#: lib/mv_web/translations/member_fields.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Membership Fee Start Date" +msgstr "Mitgliedsbeitragsstatus" + +#: lib/mv_web/live/components/field_visibility_dropdown_component.ex #, elixir-autogen, elixir-format -msgid "Confirmation text does not match" -msgstr "" +msgid "Show/Hide Columns" +msgstr "Spalten ein-/ausblenden" #: lib/mv_web/live/member_live/show/membership_fees_component.ex #, elixir-autogen, elixir-format, fuzzy -msgid "No cycles to delete" -msgstr "Keine Zyklen" - -#: lib/mv_web/live/member_live/show.ex -#, elixir-autogen, elixir-format -msgid "Not set" -msgstr "Nicht gesetzt" - -#~ #: lib/mv_web/live/components/payment_filter_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "All payment statuses" -#~ msgstr "Jeder Zahlungs-Zustand" - -#~ #: lib/mv_web/live/custom_field_live/show.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Auto-generated identifier (immutable)" -#~ msgstr "Automatisch generierter Bezeichner (unveränderlich)" - -#~ #: lib/mv_web/live/contribution_settings_live.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Configure global settings for membership contributions." -#~ msgstr "Globale Einstellungen für Mitgliedsbeiträge konfigurieren." - -#~ #: lib/mv_web/live/member_live/form.ex -#~ #: lib/mv_web/live/member_live/show.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Contribution" -#~ msgstr "Beitrag" - -#~ #: lib/mv_web/components/layouts/navbar.ex -#~ #: lib/mv_web/live/contribution_settings_live.ex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Contribution Settings" -#~ msgstr "Beitragseinstellungen" - -#~ #: lib/mv_web/live/member_field_live/index_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Field Name" -#~ msgstr "Name des Datenfelds" - -#~ #: lib/mv_web/live/member_live/index.html.heex -#~ #, elixir-autogen, elixir-format -#~ msgid "Copy emails" -#~ msgstr "E-Mails kopieren" - -#~ #: lib/mv_web/live/member_field_live/index_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Hide" -#~ msgstr "Ausblenden" - -#~ #: lib/mv_web/live/member_field_live/index_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Hide %{field} in overview" -#~ msgstr "Verstecke %{field} in der Übersicht" - -#~ #: lib/mv_web/live/contribution_settings_live.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Default Contribution Type" -#~ msgstr "Standard-Beitragsart" - -#~ #: lib/mv_web/live/member_live/show/membership_fees_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Edit amount" -#~ msgstr "Betrag bearbeiten" - -#~ #: lib/mv_web/live/member_live/show/membership_fees_component.ex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Failed to delete some cycles: %{errors}" -#~ msgstr "Konnte Feld nicht löschen: %{error}" - -#~ #: lib/mv_web/live/custom_field_live/form_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Immutable" -#~ msgstr "Unveränderlich" - -#~ #: lib/mv_web/live/contribution_settings_live.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Include joining period" -#~ msgstr "Beitrittsdatum einbeziehen" - -#~ #: lib/mv_web/live/member_field_live/index_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Show %{field} in overview" -#~ msgstr "" - -#~ #: lib/mv_web/live/custom_field_live/index_component.ex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "New Custom field" -#~ msgstr "Benutzerdefiniertes Feld speichern" - -#~ #: lib/mv_web/live/components/payment_filter_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Not paid" -#~ msgstr "Nicht bezahlt" - -#~ #: lib/mv_web/live/member_live/show.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Payment Cycle" -#~ msgstr "Zahlungszyklus" - -#~ #: lib/mv_web/live/member_live/show.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Pending" -#~ msgstr "Ausstehend" - -#~ #: lib/mv_web/live/member_live/form.ex -#~ #: lib/mv_web/live/member_live/show.ex -#~ #: lib/mv_web/translations/member_fields.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Phone" -#~ msgstr "Telefon" - -#~ #: lib/mv_web/live/member_live/index.html.heex -#~ #, elixir-autogen, elixir-format -#~ msgid "Phone Number" -#~ msgstr "Telefonnummer" - -#~ #: lib/mv_web/live/contribution_settings_live.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Quarterly Interval - Joining Period Excluded" -#~ msgstr "Vierteljährliches Intervall – Beitrittszeitraum nicht einbezogen" - -#~ #: lib/mv_web/live/member_live/index.html.heex -#~ #, elixir-autogen, elixir-format -#~ msgid "Show Last/Current Cycle Payment Status" -#~ msgstr "" +msgid "The cycle will be calculated based on this date and the interval." +msgstr "Der Zyklus wird basierend auf diesem Datum und dem Intervall berechnet." #~ #: lib/mv_web/live/member_live/index.html.heex #~ #, elixir-autogen, elixir-format @@ -2024,18 +1864,71 @@ msgstr "Nicht gesetzt" #~ #: lib/mv_web/live/member_live/index.html.heex #~ #, elixir-autogen, elixir-format -#~ msgid "Show last completed cycle" -#~ msgstr "Letzten abgeschlossenen Zyklus anzeigen" +#~ msgid "Unpaid in last cycle" +#~ msgstr "Unbezahlt im letzten Zyklus" + +#~ #: lib/mv_web/live/custom_field_live/index_component.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "New Custom field" +#~ msgstr "Benutzerdefiniertes Feld speichern" #~ #: lib/mv_web/live/member_live/index.html.heex #~ #, elixir-autogen, elixir-format -#~ msgid "Switch to current cycle" -#~ msgstr "Zum aktuellen Zyklus wechseln" +#~ msgid "Show Last/Current Cycle Payment Status" +#~ msgstr "" + +#~ #: lib/mv_web/live/components/payment_filter_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "All payment statuses" +#~ msgstr "Jeder Zahlungs-Zustand" #~ #: lib/mv_web/live/member_live/index.html.heex #~ #, elixir-autogen, elixir-format -#~ msgid "Switch to last completed cycle" -#~ msgstr "Zum letzten abgeschlossenen Zyklus wechseln" +#~ msgid "Copy emails" +#~ msgstr "E-Mails kopieren" + +#~ #: lib/mv_web/live/member_live/form.ex +#~ #: lib/mv_web/live/member_live/show.ex +#~ #: lib/mv_web/translations/member_fields.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Phone" +#~ msgstr "Telefon" + +#~ #: lib/mv_web/live/member_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Pending" +#~ msgstr "Ausstehend" + +#~ #: lib/mv_web/live/member_field_live/index_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Hide %{field} in overview" +#~ msgstr "Verstecke %{field} in der Übersicht" + +#~ #: lib/mv_web/live/member_field_live/index_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Hide" +#~ msgstr "Ausblenden" + +#~ #: lib/mv_web/live/member_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Payment Cycle" +#~ msgstr "Zahlungszyklus" + +#~ #: lib/mv_web/live/member_field_live/form_component.ex +#~ #: lib/mv_web/live/member_field_live/index_component.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "String" +#~ msgstr "Einstellungen" + +#~ #: lib/mv_web/live/contribution_settings_live.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "View Example Member" +#~ msgstr "Beispielmitglied anzeigen" + +#~ #: lib/mv_web/live/global_settings_live.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Failed to update member field visibility: %{error}" +#~ msgstr "Fehler beim anpassen der Sichtbarkeit des Feldes: %{error}" #~ #: lib/mv_web/live/member_live/form.ex #~ #: lib/mv_web/live/member_live/show.ex @@ -2043,25 +1936,66 @@ msgstr "Nicht gesetzt" #~ msgid "This data is for demonstration purposes only (mockup)." #~ msgstr "Diese Daten dienen nur zu Demonstrationszwecken (Mockup)." -#~ #: lib/mv_web/live/member_live/index.html.heex +#~ #: lib/mv_web/live/member_field_live/index_component.ex #~ #, elixir-autogen, elixir-format -#~ msgid "Unpaid in current cycle" -#~ msgstr "Unbezahlt im aktuellen Zyklus" +#~ msgid "Show %{field} in overview" +#~ msgstr "" -#~ #: lib/mv_web/live/member_live/index.html.heex +#~ #: lib/mv_web/live/member_live/show/membership_fees_component.ex #~ #, elixir-autogen, elixir-format -#~ msgid "Unpaid in last cycle" -#~ msgstr "Unbezahlt im letzten Zyklus" +#~ msgid "Edit amount" +#~ msgstr "Betrag bearbeiten" + +#~ #: lib/mv_web/live/member_live/show/membership_fees_component.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "Delete cycle" +#~ msgstr "Zyklus löschen" + +#~ #: lib/mv_web/live/member_field_live/form_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Back to member field overview" +#~ msgstr "" #~ #: lib/mv_web/live/contribution_settings_live.ex #~ #, elixir-autogen, elixir-format -#~ msgid "View Example Member" -#~ msgstr "Beispielmitglied anzeigen" +#~ msgid "Quarterly Interval - Joining Period Excluded" +#~ msgstr "Vierteljährliches Intervall – Beitrittszeitraum nicht einbezogen" + +#~ #: lib/mv_web/live/member_live/show/membership_fees_component.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "Failed to delete some cycles: %{errors}" +#~ msgstr "Konnte Feld nicht löschen: %{error}" + +#~ #: lib/mv_web/live/member_field_live/form_component.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "Save Member Field" +#~ msgstr "Mitglied speichern" + +#~ #: lib/mv_web/live/member_live/index.html.heex +#~ #, elixir-autogen, elixir-format +#~ msgid "Switch to current cycle" +#~ msgstr "Zum aktuellen Zyklus wechseln" + +#~ #: lib/mv_web/live/member_live/show/membership_fees_component.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "Delete all cycles" +#~ msgstr "Zyklus löschen" + +#~ #: lib/mv_web/components/layouts/navbar.ex +#~ #: lib/mv_web/live/contribution_settings_live.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "Contribution Settings" +#~ msgstr "Beitragseinstellungen" + +#~ #: lib/mv_web/live/member_live/show/membership_fees_component.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "Cycle Period" +#~ msgstr "Zyklus" #~ #: lib/mv_web/live/contribution_settings_live.ex #~ #, elixir-autogen, elixir-format -#~ msgid "Yearly Interval - Joining Period Included" -#~ msgstr "Jährliches Intervall – Beitrittszeitraum einbezogen" +#~ msgid "Include joining period" +#~ msgstr "Beitrittsdatum einbeziehen" #~ #: lib/mv_web/live/member_live/form.ex #~ #: lib/mv_web/live/member_live/show.ex @@ -2069,7 +2003,84 @@ msgstr "Nicht gesetzt" #~ msgid "monthly" #~ msgstr "monatlich" +#~ #: lib/mv_web/live/member_field_live/form_component.ex +#~ #: lib/mv_web/live/member_field_live/index_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Boolean" +#~ msgstr "Ja/Nein Wert" + +#~ #: lib/mv_web/live/member_live/index.html.heex +#~ #, elixir-autogen, elixir-format +#~ msgid "Show last completed cycle" +#~ msgstr "Letzten abgeschlossenen Zyklus anzeigen" + +#~ #: lib/mv_web/live/member_field_live/index_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Field Name" +#~ msgstr "Name des Datenfelds" + +#~ #: lib/mv_web/live/components/field_visibility_dropdown_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Columns" +#~ msgstr "Spalten" + +#~ #: lib/mv_web/live/global_settings_live.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Member field visibility updated successfully" +#~ msgstr "Sichtbarkeit des Feldes erfolgreich aktualisiert." + +#~ #: lib/mv_web/live/components/payment_filter_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Not paid" +#~ msgstr "Nicht bezahlt" + +#~ #: lib/mv_web/live/contribution_settings_live.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Yearly Interval - Joining Period Included" +#~ msgstr "Jährliches Intervall – Beitrittszeitraum einbezogen" + +#~ #: lib/mv_web/live/member_field_live/form_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Immutable" +#~ msgstr "Unveränderlich" + +#~ #: lib/mv_web/live/member_live/form.ex +#~ #: lib/mv_web/live/member_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Contribution" +#~ msgstr "Beitrag" + +#~ #: lib/mv_web/live/member_live/index.html.heex +#~ #, elixir-autogen, elixir-format +#~ msgid "Switch to last completed cycle" +#~ msgstr "Zum letzten abgeschlossenen Zyklus wechseln" + +#~ #: lib/mv_web/live/contribution_settings_live.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Configure global settings for membership contributions." +#~ msgstr "Globale Einstellungen für Mitgliedsbeiträge konfigurieren." + +#~ #: lib/mv_web/live/custom_field_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Auto-generated identifier (immutable)" +#~ msgstr "Automatisch generierter Bezeichner (unveränderlich)" + +#~ #: lib/mv_web/live/contribution_settings_live.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Default Contribution Type" +#~ msgstr "Standard-Beitragsart" + #~ #: lib/mv_web/live/member_live/form.ex #~ #, elixir-autogen, elixir-format #~ msgid "yearly" #~ msgstr "jährlich" + +#~ #: lib/mv_web/live/member_live/index.html.heex +#~ #, elixir-autogen, elixir-format +#~ msgid "Phone Number" +#~ msgstr "Telefonnummer" + +#~ #: lib/mv_web/live/member_live/index.html.heex +#~ #, elixir-autogen, elixir-format +#~ msgid "Unpaid in current cycle" +#~ msgstr "Unbezahlt im aktuellen Zyklus" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index e6c520e..6e2df25 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -123,6 +123,7 @@ msgid "close" msgstr "" #: lib/mv_web/live/member_live/form.ex +#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/show.ex #: lib/mv_web/translations/member_fields.ex #, elixir-autogen, elixir-format @@ -184,7 +185,6 @@ msgstr "" #: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/member_field_live/index_component.ex -#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index/formatter.ex #: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format @@ -198,7 +198,6 @@ msgstr "" #: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/member_field_live/index_component.ex -#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index/formatter.ex #: lib/mv_web/live/member_live/show.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex @@ -276,6 +275,7 @@ msgstr "" #: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/member_field_live/form_component.ex #: lib/mv_web/live/member_field_live/index_component.ex +#: lib/mv_web/live/membership_fee_type_live/form.ex #, elixir-autogen, elixir-format msgid "Description" msgstr "" @@ -290,12 +290,6 @@ msgstr "" msgid "Enabled" msgstr "" -#: lib/mv_web/live/custom_field_live/form_component.ex -#: lib/mv_web/live/member_field_live/form_component.ex -#, elixir-autogen, elixir-format -msgid "Immutable" -msgstr "" - #: lib/mv_web/components/layouts/navbar.ex #, elixir-autogen, elixir-format msgid "Logout" @@ -326,6 +320,8 @@ msgstr "" #: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/member_field_live/form_component.ex #: lib/mv_web/live/member_field_live/index_component.ex +#: lib/mv_web/live/membership_fee_type_live/form.ex +#: lib/mv_web/live/membership_fee_type_live/index.ex #, elixir-autogen, elixir-format msgid "Name" msgstr "" @@ -628,7 +624,6 @@ msgstr "" msgid "Please select a custom field first" msgstr "" -#: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/member_live/form.ex #: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format @@ -891,6 +886,7 @@ msgid "Amount" msgstr "" #: lib/mv_web/live/contribution_period_live/show.ex +#: lib/mv_web/live/member_field_live/form_component.ex #, elixir-autogen, elixir-format msgid "Back to Settings" msgstr "" @@ -926,11 +922,6 @@ msgstr "" msgid "Contribution type" msgstr "" -#: lib/mv_web/live/contribution_type_live/index.ex -#, elixir-autogen, elixir-format -msgid "Contribution types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation." -msgstr "" - #: lib/mv_web/components/layouts/navbar.ex #, elixir-autogen, elixir-format msgid "Contributions" @@ -1228,11 +1219,6 @@ msgstr "" msgid "Yearly" msgstr "" -#: lib/mv_web/live/components/field_visibility_dropdown_component.ex -#, elixir-autogen, elixir-format -msgid "Columns" -msgstr "" - #: lib/mv_web/live/components/field_visibility_dropdown_component.ex #, elixir-autogen, elixir-format msgid "Custom Field %{id}" @@ -1306,8 +1292,7 @@ msgstr "" msgid "Value Type" msgstr "" -#: lib/mv_web/live/member_field_live/form_component.ex -#: lib/mv_web/live/member_field_live/index_component.ex +#: lib/mv_web/live/member_live/show/membership_fees_component.ex #: lib/mv_web/translations/field_types.ex #, elixir-autogen, elixir-format msgid "Date" @@ -1335,16 +1320,6 @@ msgstr "" #: lib/mv_web/live/global_settings_live.ex #, elixir-autogen, elixir-format -msgid "Failed to update member field visibility: %{error}" -msgstr "" - -#: lib/mv_web/live/global_settings_live.ex -#, elixir-autogen, elixir-format -msgid "Member field visibility updated successfully" -msgstr "" - -#: lib/mv_web/live/member_field_live/index_component.ex -#, elixir-autogen, elixir-format msgid "Memberdata" msgstr "" @@ -1359,34 +1334,526 @@ msgstr "" msgid "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview." msgstr "" -#: lib/mv_web/live/member_field_live/form_component.ex -#, elixir-autogen, elixir-format -msgid "Back to member field overview" -msgstr "" - -#: lib/mv_web/live/member_field_live/form_component.ex -#: lib/mv_web/live/member_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "Boolean" -msgstr "" - -#: lib/mv_web/live/member_field_live/form_component.ex -#, elixir-autogen, elixir-format -msgid "Edit Member Field: %{field}" -msgstr "" - #: lib/mv_web/live/global_settings_live.ex #, elixir-autogen, elixir-format msgid "Member field %{action} successfully" msgstr "" -#: lib/mv_web/live/member_field_live/form_component.ex +#: lib/mv_web/live/member_live/show/membership_fees_component.ex #, elixir-autogen, elixir-format -msgid "Save Member Field" +msgid "A cycle for this period already exists" +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/index.ex +#, elixir-autogen, elixir-format +msgid "About Membership Fee Types" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "All cycles deleted" +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/form.ex +#, elixir-autogen, elixir-format +msgid "Already paid cycles will remain with the old amount." +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#: lib/mv_web/live/membership_fee_type_live/index.ex +#, elixir-autogen, elixir-format +msgid "An error occurred" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Are you sure you want to delete this cycle?" +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/index.ex +#, elixir-autogen, elixir-format +msgid "Cannot delete - %{count} member(s) assigned" +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/form.ex +#, elixir-autogen, elixir-format +msgid "Change Amount?" +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/form.ex +#, elixir-autogen, elixir-format +msgid "Changing the amount will affect %{count} member(s)." +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Click to edit amount" +msgstr "" + +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Configure global settings for membership fees." +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/form.ex +#, elixir-autogen, elixir-format +msgid "Confirm Change" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Confirmation text does not match" +msgstr "" + +#: lib/mv_web/live/member_live/index.html.heex +#, elixir-autogen, elixir-format +msgid "Copy email addresses" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Create" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Create Cycle" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Create a new cycle manually" +msgstr "" + +#: lib/mv_web/live/member_live/show.ex +#, elixir-autogen, elixir-format +msgid "Current Cycle" +msgstr "" + +#: lib/mv_web/live/member_live/index.html.heex +#, elixir-autogen, elixir-format +msgid "Current Cycle Payment Status" +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/form.ex +#, elixir-autogen, elixir-format +msgid "Current amount" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Cycle" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Cycle amount updated" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Cycle created successfully" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Cycle deleted" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Cycle status updated" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Cycles regenerated successfully" +msgstr "" + +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Default Membership Fee Type" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Delete All" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Delete All Cycles" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Delete Cycle" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Edit Cycle Amount" msgstr "" #: lib/mv_web/live/member_field_live/form_component.ex -#: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format -msgid "String" +msgid "Edit Field: %{field}" +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/form.ex +#, elixir-autogen, elixir-format +msgid "Edit Membership Fee Type" +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/index.ex +#, elixir-autogen, elixir-format +msgid "Edit membership fee type" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Failed to update cycle status: %{errors}" +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/form.ex +#, elixir-autogen, elixir-format +msgid "Future unpaid cycles will be regenerated with the new amount." +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Generate cycles from the last existing cycle to today" +msgstr "" + +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Generated cycles" +msgstr "" + +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Include joining cycle" +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/form.ex +#, elixir-autogen, elixir-format +msgid "Interval cannot be changed after creation." +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Invalid amount format" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Invalid date format" +msgstr "" + +#: lib/mv_web/live/member_live/show.ex +#, elixir-autogen, elixir-format +msgid "Last Cycle" +msgstr "" + +#: lib/mv_web/live/member_live/index.html.heex +#, elixir-autogen, elixir-format +msgid "Last Cycle Payment Status" +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/index.ex +#, elixir-autogen, elixir-format +msgid "Manage membership fee types for membership fees." +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Mark as paid" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Mark as suspended" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Mark as unpaid" +msgstr "" + +#: lib/mv_web/live/member_live/form.ex +#: lib/mv_web/live/member_live/show.ex +#, elixir-autogen, elixir-format +msgid "Membership Fee" +msgstr "" + +#: lib/mv_web/components/layouts/navbar.ex +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Membership Fee Settings" +msgstr "" + +#: lib/mv_web/live/member_live/index.html.heex +#, elixir-autogen, elixir-format +msgid "Membership Fee Status" +msgstr "" + +#: lib/mv_web/live/member_live/form.ex +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Membership Fee Type" +msgstr "" + +#: lib/mv_web/components/layouts/navbar.ex +#: lib/mv_web/live/membership_fee_type_live/index.ex +#, elixir-autogen, elixir-format +msgid "Membership Fee Types" +msgstr "" + +#: lib/mv_web/live/member_live/show.ex +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Membership Fees" +msgstr "" + +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Membership fee start" +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/index.ex +#, elixir-autogen, elixir-format +msgid "Membership fee type deleted" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Membership fee type removed" +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/form.ex +#, elixir-autogen, elixir-format +msgid "Membership fee type saved successfully" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Membership fee type updated. Cycles regenerated." +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/index.ex +#, elixir-autogen, elixir-format +msgid "Membership fee types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation." +msgstr "" + +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Monthly Interval - Joining Cycle Included" +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/form.ex +#: lib/mv_web/live/membership_fee_type_live/index.ex +#, elixir-autogen, elixir-format +msgid "New Membership Fee Type" +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/form.ex +#, elixir-autogen, elixir-format +msgid "New amount" +msgstr "" + +#: lib/mv_web/live/member_live/index.html.heex +#, elixir-autogen, elixir-format +msgid "No cycle" +msgstr "" + +#: lib/mv_web/live/member_live/show.ex +#, elixir-autogen, elixir-format +msgid "No cycles" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "No cycles to delete" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "No membership fee cycles found. Cycles will be generated automatically when a membership fee type is assigned." +msgstr "" + +#: lib/mv_web/live/member_live/show.ex +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "No membership fee type assigned" +msgstr "" + +#: lib/mv_web/live/member_live/show.ex +#, elixir-autogen, elixir-format +msgid "No status" +msgstr "" + +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "None (no default)" +msgstr "" + +#: lib/mv_web/live/member_live/show.ex +#, elixir-autogen, elixir-format +msgid "Not set" +msgstr "" + +#: lib/mv_web/live/member_live/show.ex +#, elixir-autogen, elixir-format +msgid "Payment Interval" +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/form.ex +#, elixir-autogen, elixir-format +msgid "Please confirm the amount change first" +msgstr "" + +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Quarterly Interval - Joining Cycle Excluded" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Regenerate Cycles" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Regenerating..." +msgstr "" + +#: lib/mv_web/live/custom_field_live/form_component.ex +#, elixir-autogen, elixir-format +msgid "Save Custom Field" +msgstr "" + +#: lib/mv_web/live/custom_field_value_live/form.ex +#, elixir-autogen, elixir-format +msgid "Save Custom Field Value" +msgstr "" + +#: lib/mv_web/live/member_field_live/form_component.ex +#, elixir-autogen, elixir-format +msgid "Save Field" +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/form.ex +#, elixir-autogen, elixir-format +msgid "Save Membership Fee Type" +msgstr "" + +#: lib/mv_web/live/member_live/form.ex +#, elixir-autogen, elixir-format +msgid "Select a membership fee type for this member. Members can only switch between types with the same interval." +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/form.ex +#, elixir-autogen, elixir-format +msgid "Select interval" +msgstr "" + +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Settings saved successfully." +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "This action cannot be undone." +msgstr "" + +#: lib/mv_web/components/core_components.ex +#, elixir-autogen, elixir-format +msgid "This field is required" +msgstr "" + +#: lib/mv_web/live/member_field_live/form_component.ex +#, elixir-autogen, elixir-format +msgid "This is a technical field and cannot be changed" +msgstr "" + +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "This membership fee type is automatically assigned to all new members. Can be changed individually per member." +msgstr "" + +#: lib/mv_web/live/member_live/show.ex +#, elixir-autogen, elixir-format +msgid "Type" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Type '%{confirmation}' to confirm" +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/form.ex +#, elixir-autogen, elixir-format +msgid "Use this form to manage membership fee types in your database." +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Warning" +msgstr "" + +#: lib/mv_web/live/member_live/form.ex +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Warning: Changing from %{old_interval} to %{new_interval} is not allowed. Please select a membership fee type with the same interval." +msgstr "" + +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "When active: Members pay from the cycle of their joining." +msgstr "" + +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "When inactive: Members pay from the next full cycle after joining." +msgstr "" + +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Yearly Interval - Joining Cycle Excluded" +msgstr "" + +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Yearly Interval - Joining Cycle Included" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "You are about to delete all %{count} cycles for this member." +msgstr "" + +#: lib/mv_web/live/contribution_type_live/index.ex +#, elixir-autogen, elixir-format +msgid "Contribution types define different membership fee structures. Each type has a fixed cycle (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation." +msgstr "" + +#: lib/mv_web/live/membership_fee_type_live/index.ex +#, elixir-autogen, elixir-format +msgid "Delete Membership Fee Type" +msgstr "" + +#: lib/mv_web/translations/member_fields.ex +#, elixir-autogen, elixir-format +msgid "Membership Fee Start Date" +msgstr "" + +#: lib/mv_web/live/components/field_visibility_dropdown_component.ex +#, elixir-autogen, elixir-format +msgid "Show/Hide Columns" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "The cycle will be calculated based on this date and the interval." msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 26a5599..467e715 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -123,6 +123,7 @@ msgid "close" msgstr "" #: lib/mv_web/live/member_live/form.ex +#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/show.ex #: lib/mv_web/translations/member_fields.ex #, elixir-autogen, elixir-format @@ -289,12 +290,6 @@ msgstr "" msgid "Enabled" msgstr "" -#: lib/mv_web/live/custom_field_live/form_component.ex -#: lib/mv_web/live/member_field_live/form_component.ex -#, elixir-autogen, elixir-format -msgid "Immutable" -msgstr "" - #: lib/mv_web/components/layouts/navbar.ex #, elixir-autogen, elixir-format msgid "Logout" @@ -629,7 +624,6 @@ msgstr "" msgid "Please select a custom field first" msgstr "" -#: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/member_live/form.ex #: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format, fuzzy @@ -892,6 +886,7 @@ msgid "Amount" msgstr "" #: lib/mv_web/live/contribution_period_live/show.ex +#: lib/mv_web/live/member_field_live/form_component.ex #, elixir-autogen, elixir-format msgid "Back to Settings" msgstr "" @@ -927,11 +922,6 @@ msgstr "" msgid "Contribution type" msgstr "" -#: lib/mv_web/live/contribution_type_live/index.ex -#, elixir-autogen, elixir-format -msgid "Contribution types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation." -msgstr "" - #: lib/mv_web/components/layouts/navbar.ex #, elixir-autogen, elixir-format msgid "Contributions" @@ -1229,11 +1219,6 @@ msgstr "" msgid "Yearly" msgstr "" -#: lib/mv_web/live/components/field_visibility_dropdown_component.ex -#, elixir-autogen, elixir-format -msgid "Columns" -msgstr "" - #: lib/mv_web/live/components/field_visibility_dropdown_component.ex #, elixir-autogen, elixir-format, fuzzy msgid "Custom Field %{id}" @@ -1307,8 +1292,6 @@ msgstr "" msgid "Value Type" msgstr "" -#: lib/mv_web/live/member_field_live/form_component.ex -#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex #: lib/mv_web/translations/field_types.ex #, elixir-autogen, elixir-format @@ -1336,16 +1319,6 @@ msgid "Yes/No-Selection" msgstr "" #: lib/mv_web/live/global_settings_live.ex -#, elixir-autogen, elixir-format -msgid "Failed to update member field visibility: %{error}" -msgstr "" - -#: lib/mv_web/live/global_settings_live.ex -#, elixir-autogen, elixir-format -msgid "Member field visibility updated successfully" -msgstr "" - -#: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format, fuzzy msgid "Memberdata" msgstr "" @@ -1361,36 +1334,14 @@ msgstr "" msgid "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview." msgstr "" -#: lib/mv_web/live/member_field_live/form_component.ex -#, elixir-autogen, elixir-format -msgid "Back to member field overview" -msgstr "" - -#: lib/mv_web/live/member_field_live/form_component.ex -#: lib/mv_web/live/member_field_live/index_component.ex -#, elixir-autogen, elixir-format -msgid "Boolean" -msgstr "" - -#: lib/mv_web/live/member_field_live/form_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Edit Member Field: %{field}" -msgstr "" - #: lib/mv_web/live/global_settings_live.ex #, elixir-autogen, elixir-format, fuzzy msgid "Member field %{action} successfully" msgstr "" -#: lib/mv_web/live/member_field_live/form_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Save Member Field" -msgstr "" - -#: lib/mv_web/live/member_field_live/form_component.ex -#: lib/mv_web/live/member_field_live/index_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "String" +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "A cycle for this period already exists" msgstr "" #: lib/mv_web/live/membership_fee_type_live/index.ex @@ -1398,6 +1349,11 @@ msgstr "" msgid "About Membership Fee Types" msgstr "" +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "All cycles deleted" +msgstr "" + #: lib/mv_web/live/membership_fee_type_live/form.ex #, elixir-autogen, elixir-format msgid "Already paid cycles will remain with the old amount." @@ -1429,16 +1385,56 @@ msgstr "" msgid "Changing the amount will affect %{count} member(s)." msgstr "" +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Click to edit amount" +msgstr "" + +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Configure global settings for membership fees." +msgstr "" + #: lib/mv_web/live/membership_fee_type_live/form.ex #, elixir-autogen, elixir-format msgid "Confirm Change" msgstr "" +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Confirmation text does not match" +msgstr "" + +#: lib/mv_web/live/member_live/index.html.heex +#, elixir-autogen, elixir-format, fuzzy +msgid "Copy email addresses" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Create" +msgstr "created" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Create Cycle" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Create a new cycle manually" +msgstr "" + #: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format, fuzzy msgid "Current Cycle" msgstr "" +#: lib/mv_web/live/member_live/index.html.heex +#, elixir-autogen, elixir-format +msgid "Current Cycle Payment Status" +msgstr "Current Cycle Payment Status" + #: lib/mv_web/live/membership_fee_type_live/form.ex #, elixir-autogen, elixir-format, fuzzy msgid "Current amount" @@ -1454,6 +1450,11 @@ msgstr "" msgid "Cycle amount updated" msgstr "" +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Cycle created successfully" +msgstr "" + #: lib/mv_web/live/member_live/show/membership_fees_component.ex #, elixir-autogen, elixir-format msgid "Cycle deleted" @@ -1469,6 +1470,21 @@ msgstr "" msgid "Cycles regenerated successfully" msgstr "" +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Default Membership Fee Type" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Delete All" +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Delete All Cycles" +msgstr "" + #: lib/mv_web/live/member_live/show/membership_fees_component.ex #, elixir-autogen, elixir-format, fuzzy msgid "Delete Cycle" @@ -1479,11 +1495,21 @@ msgstr "" msgid "Edit Cycle Amount" msgstr "" +#: lib/mv_web/live/member_field_live/form_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Edit Field: %{field}" +msgstr "" + #: lib/mv_web/live/membership_fee_type_live/form.ex #, elixir-autogen, elixir-format, fuzzy msgid "Edit Membership Fee Type" msgstr "" +#: lib/mv_web/live/membership_fee_type_live/index.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Edit membership fee type" +msgstr "" + #: lib/mv_web/live/member_live/show/membership_fees_component.ex #, elixir-autogen, elixir-format msgid "Failed to update cycle status: %{errors}" @@ -1499,6 +1525,16 @@ msgstr "" msgid "Generate cycles from the last existing cycle to today" msgstr "" +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Generated cycles" +msgstr "" + +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Include joining cycle" +msgstr "" + #: lib/mv_web/live/membership_fee_type_live/form.ex #, elixir-autogen, elixir-format msgid "Interval cannot be changed after creation." @@ -1509,11 +1545,21 @@ msgstr "" msgid "Invalid amount format" msgstr "" +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Invalid date format" +msgstr "" + #: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "Last Cycle" msgstr "" +#: lib/mv_web/live/member_live/index.html.heex +#, elixir-autogen, elixir-format +msgid "Last Cycle Payment Status" +msgstr "Last Cycle Payment Status" + #: lib/mv_web/live/membership_fee_type_live/index.ex #, elixir-autogen, elixir-format msgid "Manage membership fee types for membership fees." @@ -1540,6 +1586,12 @@ msgstr "" msgid "Membership Fee" msgstr "" +#: lib/mv_web/components/layouts/navbar.ex +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Membership Fee Settings" +msgstr "" + #: lib/mv_web/live/member_live/index.html.heex #, elixir-autogen, elixir-format, fuzzy msgid "Membership Fee Status" @@ -1563,6 +1615,11 @@ msgstr "" msgid "Membership Fees" msgstr "" +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Membership fee start" +msgstr "" + #: lib/mv_web/live/membership_fee_type_live/index.ex #, elixir-autogen, elixir-format, fuzzy msgid "Membership fee type deleted" @@ -1588,6 +1645,11 @@ msgstr "" msgid "Membership fee types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation." msgstr "" +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "Monthly Interval - Joining Cycle Included" +msgstr "" + #: lib/mv_web/live/membership_fee_type_live/form.ex #: lib/mv_web/live/membership_fee_type_live/index.ex #, elixir-autogen, elixir-format, fuzzy @@ -1609,6 +1671,11 @@ msgstr "" msgid "No cycles" msgstr "" +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "No cycles to delete" +msgstr "" + #: lib/mv_web/live/member_live/show/membership_fees_component.ex #, elixir-autogen, elixir-format msgid "No membership fee cycles found. Cycles will be generated automatically when a membership fee type is assigned." @@ -1625,11 +1692,31 @@ msgstr "" msgid "No status" msgstr "" +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "None (no default)" +msgstr "" + +#: lib/mv_web/live/member_live/show.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Not set" +msgstr "" + +#: lib/mv_web/live/member_live/show.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Payment Interval" +msgstr "" + #: lib/mv_web/live/membership_fee_type_live/form.ex #, elixir-autogen, elixir-format msgid "Please confirm the amount change first" msgstr "" +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Quarterly Interval - Joining Cycle Excluded" +msgstr "" + #: lib/mv_web/live/member_live/show/membership_fees_component.ex #, elixir-autogen, elixir-format msgid "Regenerate Cycles" @@ -1640,6 +1727,21 @@ msgstr "" msgid "Regenerating..." msgstr "" +#: lib/mv_web/live/custom_field_live/form_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Save Custom Field" +msgstr "" + +#: lib/mv_web/live/custom_field_value_live/form.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Save Custom Field Value" +msgstr "" + +#: lib/mv_web/live/member_field_live/form_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Save Field" +msgstr "" + #: lib/mv_web/live/membership_fee_type_live/form.ex #, elixir-autogen, elixir-format, fuzzy msgid "Save Membership Fee Type" @@ -1655,110 +1757,75 @@ msgstr "" msgid "Select interval" msgstr "" +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Settings saved successfully." +msgstr "" + +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "This action cannot be undone." +msgstr "" + +#: lib/mv_web/components/core_components.ex +#, elixir-autogen, elixir-format +msgid "This field is required" +msgstr "" + +#: lib/mv_web/live/member_field_live/form_component.ex +#, elixir-autogen, elixir-format +msgid "This is a technical field and cannot be changed" +msgstr "" + +#: lib/mv_web/live/membership_fee_settings_live.ex +#, elixir-autogen, elixir-format +msgid "This membership fee type is automatically assigned to all new members. Can be changed individually per member." +msgstr "" + #: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "Type" msgstr "" +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Type '%{confirmation}' to confirm" +msgstr "" + #: lib/mv_web/live/membership_fee_type_live/form.ex #, elixir-autogen, elixir-format, fuzzy msgid "Use this form to manage membership fee types in your database." msgstr "" +#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#, elixir-autogen, elixir-format +msgid "Warning" +msgstr "" + #: lib/mv_web/live/member_live/form.ex #: lib/mv_web/live/member_live/show/membership_fees_component.ex #, elixir-autogen, elixir-format msgid "Warning: Changing from %{old_interval} to %{new_interval} is not allowed. Please select a membership fee type with the same interval." msgstr "" -#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#: lib/mv_web/live/membership_fee_settings_live.ex #, elixir-autogen, elixir-format -msgid "A cycle for this period already exists" +msgid "When active: Members pay from the cycle of their joining." msgstr "" -#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#: lib/mv_web/live/membership_fee_settings_live.ex #, elixir-autogen, elixir-format -msgid "All cycles deleted" +msgid "When inactive: Members pay from the next full cycle after joining." msgstr "" -#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#: lib/mv_web/live/membership_fee_settings_live.ex #, elixir-autogen, elixir-format -msgid "Click to edit amount" +msgid "Yearly Interval - Joining Cycle Excluded" msgstr "" -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Create" -msgstr "created" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Create Cycle" -msgstr "" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#: lib/mv_web/live/membership_fee_settings_live.ex #, elixir-autogen, elixir-format -msgid "Create a new cycle manually" -msgstr "" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Cycle Period" -msgstr "" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Cycle created successfully" -msgstr "" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Delete All" -msgstr "" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Delete All Cycles" -msgstr "" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Delete all cycles" -msgstr "" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Delete cycle" -msgstr "" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Invalid date format" -msgstr "" - -#: lib/mv_web/live/member_live/show.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Payment Interval" -msgstr "" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format -msgid "The cycle period will be calculated based on this date and the interval." -msgstr "" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format -msgid "This action cannot be undone." -msgstr "" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format -msgid "Type '%{confirmation}' to confirm" -msgstr "" - -#: lib/mv_web/live/member_live/show/membership_fees_component.ex -#, elixir-autogen, elixir-format -msgid "Warning" +msgid "Yearly Interval - Joining Cycle Included" msgstr "" #: lib/mv_web/live/member_live/show/membership_fees_component.ex @@ -1766,42 +1833,33 @@ msgstr "" msgid "You are about to delete all %{count} cycles for this member." msgstr "" -#: lib/mv_web/live/member_live/index.html.heex -#, elixir-autogen, elixir-format -msgid "Current Cycle Payment Status" -msgstr "Current Cycle Payment Status" - -#: lib/mv_web/live/member_live/index.html.heex -#, elixir-autogen, elixir-format -msgid "Last Cycle Payment Status" -msgstr "Last Cycle Payment Status" - -#: lib/mv_web/live/membership_fee_type_live/index.ex -#, elixir-autogen, elixir-format -msgid "Delete membership fee type" +#: lib/mv_web/live/contribution_type_live/index.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Contribution types define different membership fee structures. Each type has a fixed cycle (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation." msgstr "" #: lib/mv_web/live/membership_fee_type_live/index.ex #, elixir-autogen, elixir-format, fuzzy -msgid "Edit membership fee type" +msgid "Delete Membership Fee Type" msgstr "" -#: lib/mv_web/live/member_live/show/membership_fees_component.ex +#: lib/mv_web/translations/member_fields.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Membership Fee Start Date" +msgstr "" + +#: lib/mv_web/live/components/field_visibility_dropdown_component.ex #, elixir-autogen, elixir-format -msgid "Confirmation text does not match" +msgid "Show/Hide Columns" msgstr "" #: lib/mv_web/live/member_live/show/membership_fees_component.ex #, elixir-autogen, elixir-format, fuzzy -msgid "No cycles to delete" -msgstr "" - -#: lib/mv_web/live/member_live/show.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Not set" +msgid "The cycle will be calculated based on this date and the interval." msgstr "" #~ #: lib/mv_web/live/member_live/index.html.heex +#~ #: lib/mv_web/live/components/payment_filter_component.ex #~ #, elixir-autogen, elixir-format #~ msgid "Show current cycle" #~ msgstr "" @@ -1817,6 +1875,8 @@ msgstr "" #~ msgstr "" #~ #: lib/mv_web/live/member_live/index.html.heex +#~ #: lib/mv_web/live/member_live/form.ex +#~ #: lib/mv_web/live/member_live/show.ex #~ #, elixir-autogen, elixir-format #~ msgid "Show Last/Current Cycle Payment Status" #~ msgstr "" @@ -1826,11 +1886,6 @@ msgstr "" #~ msgid "All payment statuses" #~ msgstr "" -#~ #: lib/mv_web/live/member_field_live/index_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Field Name" -#~ msgstr "" - #~ #: lib/mv_web/live/member_live/index.html.heex #~ #, elixir-autogen, elixir-format #~ msgid "Copy emails" @@ -1848,9 +1903,9 @@ msgstr "" #~ msgid "Pending" #~ msgstr "" -#~ #: lib/mv_web/live/member_live/show.ex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Payment Cycle" +#~ #: lib/mv_web/live/member_field_live/index_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Hide %{field} in overview" #~ msgstr "" #~ #: lib/mv_web/live/member_field_live/index_component.ex @@ -1858,9 +1913,15 @@ msgstr "" #~ msgid "Hide" #~ msgstr "" +#~ #: lib/mv_web/live/member_live/show.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "Payment Cycle" +#~ msgstr "" + +#~ #: lib/mv_web/live/member_field_live/form_component.ex #~ #: lib/mv_web/live/member_field_live/index_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Hide %{field} in overview" +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "String" #~ msgstr "" #~ #: lib/mv_web/live/contribution_settings_live.ex @@ -1868,21 +1929,36 @@ msgstr "" #~ msgid "View Example Member" #~ msgstr "" +#~ #: lib/mv_web/live/global_settings_live.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Failed to update member field visibility: %{error}" +#~ msgstr "" + #~ #: lib/mv_web/live/member_live/form.ex #~ #: lib/mv_web/live/member_live/show.ex #~ #, elixir-autogen, elixir-format #~ msgid "This data is for demonstration purposes only (mockup)." #~ msgstr "" +#~ #: lib/mv_web/live/member_field_live/index_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Show %{field} in overview" +#~ msgstr "" + #~ #: lib/mv_web/live/contribution_settings_live.ex #~ #: lib/mv_web/live/member_live/show/membership_fees_component.ex #~ #, elixir-autogen, elixir-format #~ msgid "Edit amount" #~ msgstr "" -#~ #: lib/mv_web/live/contribution_settings_live.ex +#~ #: lib/mv_web/live/member_live/show/membership_fees_component.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "Delete cycle" +#~ msgstr "" + +#~ #: lib/mv_web/live/member_field_live/form_component.ex #~ #, elixir-autogen, elixir-format -#~ msgid "Quarterly Interval - Joining Period Excluded" +#~ msgid "Back to member field overview" #~ msgstr "" #~ #: lib/mv_web/live/contribution_settings_live.ex @@ -1900,6 +1976,11 @@ msgstr "" #~ msgid "Switch to current cycle" #~ msgstr "" +#~ #: lib/mv_web/live/member_live/show/membership_fees_component.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "Delete all cycles" +#~ msgstr "" + #~ #: lib/mv_web/live/membership_fee_settings_live.ex #~ #, elixir-autogen, elixir-format #~ msgid "Failed to save settings. Please check the errors below." @@ -1911,41 +1992,15 @@ msgstr "" #~ msgid "Contribution Settings" #~ msgstr "" -#~ #: lib/mv_web/live/contribution_settings_live.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Include joining period" +#~ #: lib/mv_web/live/member_live/show/membership_fees_component.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "Cycle Period" #~ msgstr "" +#~ #: lib/mv_web/live/member_field_live/form_component.ex #~ #: lib/mv_web/live/member_field_live/index_component.ex #~ #, elixir-autogen, elixir-format -#~ msgid "Show %{field} in overview" -#~ msgstr "" - -#~ #: lib/mv_web/live/custom_field_live/index_component.ex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "New Custom field" -#~ msgstr "" - -#~ #: lib/mv_web/live/components/payment_filter_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Not paid" -#~ msgstr "" - -#~ #: lib/mv_web/live/member_live/form.ex -#~ #: lib/mv_web/live/member_live/show.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Quarterly Interval - Joining Period Excluded" -#~ msgstr "" - -#~ #: lib/mv_web/live/member_live/form.ex -#~ #: lib/mv_web/live/member_live/show.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Show Last/Current Cycle Payment Status" -#~ msgstr "" - -#~ #: lib/mv_web/live/components/payment_filter_component.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Show current cycle" +#~ msgid "Boolean" #~ msgstr "" #~ #: lib/mv_web/live/member_live/index.html.heex @@ -1953,14 +2008,29 @@ msgstr "" #~ msgid "Show last completed cycle" #~ msgstr "" -#~ #: lib/mv_web/live/member_live/index.html.heex +#~ #: lib/mv_web/live/member_field_live/index_component.ex #~ #, elixir-autogen, elixir-format -#~ msgid "Switch to current cycle" +#~ msgid "Field Name" #~ msgstr "" -#~ #: lib/mv_web/live/member_live/index.html.heex +#~ #: lib/mv_web/live/components/field_visibility_dropdown_component.ex #~ #, elixir-autogen, elixir-format -#~ msgid "Switch to last completed cycle" +#~ msgid "Columns" +#~ msgstr "" + +#~ #: lib/mv_web/live/global_settings_live.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Member field visibility updated successfully" +#~ msgstr "" + +#~ #: lib/mv_web/live/components/payment_filter_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Not paid" +#~ msgstr "" + +#~ #: lib/mv_web/live/member_field_live/form_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Immutable" #~ msgstr "" #~ #: lib/mv_web/live/member_live/form.ex @@ -1980,11 +2050,6 @@ msgstr "" #~ msgid "Switch to last completed cycle" #~ msgstr "" -#~ #: lib/mv_web/live/contribution_settings_live.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Configure global settings for membership contributions." -#~ msgstr "" - #~ #: lib/mv_web/live/custom_field_live/show.ex #~ #, elixir-autogen, elixir-format #~ msgid "Auto-generated identifier (immutable)" From b0623b20ede2cbeac0889c5d36f7a1722ec590fc Mon Sep 17 00:00:00 2001 From: carla Date: Thu, 8 Jan 2026 11:40:22 +0100 Subject: [PATCH 20/23] style: remove navbar fixed width --- lib/mv_web/components/layouts/navbar.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mv_web/components/layouts/navbar.ex b/lib/mv_web/components/layouts/navbar.ex index adc3444..08b0889 100644 --- a/lib/mv_web/components/layouts/navbar.ex +++ b/lib/mv_web/components/layouts/navbar.ex @@ -31,7 +31,7 @@ defmodule MvWeb.Layouts.Navbar do
  • {gettext("Contributions")} -
      +
      • <.link navigate="/membership_fee_types">{gettext("Membership Fee Types")}
      • From 6311eebb0c15b1bf54e6b6c338e2b9ef99a6cdae Mon Sep 17 00:00:00 2001 From: carla Date: Thu, 8 Jan 2026 11:41:24 +0100 Subject: [PATCH 21/23] fix linting --- lib/membership/membership.ex | 41 ++++++++++++++++++++++++ lib/mv_web/components/core_components.ex | 3 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/membership/membership.ex b/lib/membership/membership.ex index c711bcd..982b837 100644 --- a/lib/membership/membership.ex +++ b/lib/membership/membership.ex @@ -57,6 +57,9 @@ defmodule Mv.Membership do # Settings should be created via seed script define :update_settings, action: :update define :update_member_field_visibility, action: :update_member_field_visibility + + define :update_single_member_field_visibility, + action: :update_single_member_field_visibility end end @@ -186,4 +189,42 @@ defmodule Mv.Membership do }) |> Ash.update(domain: __MODULE__) end + + @doc """ + Atomically updates a single field in the member field visibility configuration. + + This action uses PostgreSQL's jsonb_set function to atomically update a single key + in the JSONB map, preventing lost updates in concurrent scenarios. This is the + preferred method for updating individual field visibility settings. + + ## Parameters + + - `settings` - The settings record to update + - `field` - The member field name as a string (e.g., "street", "house_number") + - `show_in_overview` - Boolean value indicating visibility + + ## Returns + + - `{:ok, updated_settings}` - Successfully updated settings + - `{:error, error}` - Validation or update error + + ## Examples + + iex> {:ok, settings} = Mv.Membership.get_settings() + iex> {:ok, updated} = Mv.Membership.update_single_member_field_visibility(settings, field: "street", show_in_overview: false) + iex> updated.member_field_visibility["street"] + false + + """ + def update_single_member_field_visibility(settings, + field: field, + show_in_overview: show_in_overview + ) do + settings + |> Ash.Changeset.new() + |> Ash.Changeset.set_argument(:field, field) + |> Ash.Changeset.set_argument(:show_in_overview, show_in_overview) + |> Ash.Changeset.for_update(:update_single_member_field_visibility, %{}) + |> Ash.update(domain: __MODULE__) + end end diff --git a/lib/mv_web/components/core_components.ex b/lib/mv_web/components/core_components.ex index ccec5a5..45bcae0 100644 --- a/lib/mv_web/components/core_components.ex +++ b/lib/mv_web/components/core_components.ex @@ -692,10 +692,11 @@ defmodule MvWeb.CoreComponents do """ attr :name, :string, required: true attr :class, :string, default: "size-4" + attr :rest, :global, include: ~w(aria-hidden) def icon(%{name: "hero-" <> _} = assigns) do ~H""" - + """ end From e38de7d6908db674dcb400de2a767717057653ed Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 12 Jan 2026 09:50:51 +0100 Subject: [PATCH 22/23] chore: rename custom to data field in the UI --- .../live/custom_field_live/form_component.ex | 6 +- .../live/custom_field_live/index_component.ex | 4 +- .../live/custom_field_value_live/show.ex | 6 +- lib/mv_web/live/global_settings_live.ex | 6 +- priv/gettext/de/LC_MESSAGES/default.po | 92 ++++++++++--------- priv/gettext/default.pot | 82 ++++++++--------- priv/gettext/en/LC_MESSAGES/default.po | 92 ++++++++++--------- 7 files changed, 154 insertions(+), 134 deletions(-) diff --git a/lib/mv_web/live/custom_field_live/form_component.ex b/lib/mv_web/live/custom_field_live/form_component.ex index 172cfd3..71bdd03 100644 --- a/lib/mv_web/live/custom_field_live/form_component.ex +++ b/lib/mv_web/live/custom_field_live/form_component.ex @@ -26,12 +26,12 @@ defmodule MvWeb.CustomFieldLive.FormComponent do type="button" phx-click="cancel" phx-target={@myself} - aria-label={gettext("Back to custom field overview")} + aria-label={gettext("Back to settings")} > <.icon name="hero-arrow-left" class="w-4 h-4" />

        - {if @custom_field, do: gettext("Edit Custom Field"), else: gettext("New Custom Field")} + {if @custom_field, do: gettext("Edit Data Field"), else: gettext("New Data Field")}

  • @@ -66,7 +66,7 @@ defmodule MvWeb.CustomFieldLive.FormComponent do {gettext("Cancel")} <.button phx-disable-with={gettext("Saving...")} variant="primary"> - {gettext("Save Custom Field")} + {gettext("Save Data Field")}
    diff --git a/lib/mv_web/live/custom_field_live/index_component.ex b/lib/mv_web/live/custom_field_live/index_component.ex index a11cc57..5f26f78 100644 --- a/lib/mv_web/live/custom_field_live/index_component.ex +++ b/lib/mv_web/live/custom_field_live/index_component.ex @@ -29,7 +29,7 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do phx-click="new_custom_field" phx-target={@myself} > - <.icon name="hero-plus" /> {gettext("New Custom Field")} + <.icon name="hero-plus" /> {gettext("New Data Field")}
    @@ -111,7 +111,7 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do <%!-- Delete Confirmation Modal --%>