From 3d81461fbeeb8ef8bb40357adb744bc0f9a4f998 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 15 Dec 2025 09:58:19 +0100 Subject: [PATCH] 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