250 lines
7.4 KiB
Elixir
250 lines
7.4 KiB
Elixir
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"""
|
|
<div id={@id}>
|
|
<p class="text-sm text-base-content/70 mb-4">
|
|
{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."
|
|
)}
|
|
</p>
|
|
|
|
<%!-- Show form when editing --%>
|
|
<div :if={@show_form} class="mb-8">
|
|
<.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}
|
|
/>
|
|
</div>
|
|
|
|
<%!-- 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>
|
|
|
|
<:col :let={{_field_name, field_data}} label={gettext("Value Type")}>
|
|
{format_value_type(field_data.field)}
|
|
</:col>
|
|
|
|
<:col :let={{_field_name, field_data}} label={gettext("Description")}>
|
|
{field_data.description || ""}
|
|
</:col>
|
|
|
|
<:col
|
|
:let={{_field_name, field_data}}
|
|
label={gettext("Required")}
|
|
class="max-w-[9.375rem] text-center"
|
|
>
|
|
<span
|
|
:if={@required?.(field_data.field)}
|
|
class="text-base-content font-semibold"
|
|
>
|
|
{gettext("Required")}
|
|
</span>
|
|
<span :if={!@required?.(field_data.field)} class="text-base-content/70">
|
|
{gettext("Optional")}
|
|
</span>
|
|
</:col>
|
|
|
|
<:col
|
|
:let={{_field_name, field_data}}
|
|
label={gettext("Show in overview")}
|
|
class="max-w-[9.375rem] text-center"
|
|
>
|
|
<span :if={field_data.show_in_overview} class="badge badge-success">
|
|
{gettext("Yes")}
|
|
</span>
|
|
<span :if={!field_data.show_in_overview} class="badge badge-ghost">
|
|
{gettext("No")}
|
|
</span>
|
|
</:col>
|
|
|
|
<: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")}
|
|
</.link>
|
|
</:action>
|
|
</.table>
|
|
</div>
|
|
"""
|
|
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
|