WIP: Implements settings for member fields closes #223 #300

Draft
carla wants to merge 8 commits from feature/223_memberfields_settings into main
2 changed files with 239 additions and 0 deletions
Showing only changes of commit 3d81461fbe - Show all commits

View file

@ -62,6 +62,12 @@ defmodule MvWeb.GlobalSettingsLive do
</.button>
</.form>
</.form_section>
<%!-- 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(

View file

@ -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"""
<div id={@id}>
<.form_section title={gettext("Memberdata")}>
<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. This you cannot delete these fields but hide them in the member overview.")}
</p>
<.table id="member_fields" rows={@member_fields}>
<:col :let={{_field_name, field_data}} label={gettext("Field Name")}>
{format_field_name(field_data.field)}
</: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}}>
<button
id={"member-field-#{field_data.field}-toggle"}
phx-click="toggle_field_visibility"
phx-keydown="toggle_field_visibility"
phx-key="Enter"
phx-target={@myself}
phx-value-field={field_data.field}
class="btn btn-sm btn-secondary"
aria-label={
if field_data.show_in_overview,
do:
gettext("Hide %{field} in overview", field: format_field_name(field_data.field)),
else:
gettext("Show %{field} in overview", field: format_field_name(field_data.field))
}
aria-pressed={to_string(field_data.show_in_overview)}
>
{if field_data.show_in_overview, do: gettext("Hide"), else: gettext("Show")}
</button>
</:action>
</.table>
</.form_section>
</div>
"""
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