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) - 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] @impl true def render(assigns) do assigns = assigns |> assign(:member_fields, get_member_fields_with_visibility(assigns.settings)) |> assign(:required?, &required?/1) ~H"""

{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." )}

<%!-- 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} />
<%!-- 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")} <: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("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(: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("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 field_atom = String.to_existing_atom(field_string) {:noreply, socket |> assign(:show_form, true) |> assign(:editing_member_field, field_atom) |> assign(:form_id, "member-field-form-#{field_string}")} 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) attribute = Ash.Resource.Info.attribute(Mv.Membership.Member, field) %{ 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 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 end