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..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,152 +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")} - -
-
- <%!-- 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("Show in overview")} - class="max-w-[9.375rem] text-center" +
+
+

+ {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} > - - {gettext("Yes")} - - - {gettext("No")} - - + <.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} + /> +
- <:action :let={{_id, custom_field}}> - <.link phx-click={ - JS.push("edit_custom_field", value: %{id: custom_field.id}, target: @myself) - }> - {gettext("Edit")} - - + <%!-- 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} - <: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("Value Type")}> + {@field_type_label.(custom_field.value_type)} + - <%!-- 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 0b3ec1c..bd57d55 100644 --- a/lib/mv_web/live/global_settings_live.ex +++ b/lib/mv_web/live/global_settings_live.ex @@ -62,11 +62,19 @@ defmodule MvWeb.GlobalSettingsLive do - <%!-- Custom Fields Section --%> - <.live_component - module={MvWeb.CustomFieldLive.IndexComponent} - id="custom-fields-component" - /> + <%!-- Memberdata Section --%> + <.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 @@ -125,6 +133,51 @@ 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({: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( 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 new file mode 100644 index 0000000..7422f5a --- /dev/null +++ b/lib/mv_web/live/member_field_live/index_component.ex @@ -0,0 +1,250 @@ +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 diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 25f685d..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 @@ -169,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 @@ -183,6 +185,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 +199,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 @@ -255,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 @@ -268,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" @@ -283,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" @@ -314,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" @@ -345,6 +355,9 @@ 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/form_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Required" msgstr "Erforderlich" @@ -402,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" @@ -668,6 +682,8 @@ 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" msgstr "In Übersicht anzeigen" @@ -1409,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" @@ -1438,6 +1457,64 @@ 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/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 "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)" @@ -1455,11 +1532,26 @@ msgstr "Ja/Nein-Auswahl" #~ 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 @@ -1483,6 +1575,11 @@ msgstr "Ja/Nein-Auswahl" #~ 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 a7ab36b..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 @@ -170,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 @@ -184,6 +186,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 +200,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 @@ -256,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 @@ -269,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 "" @@ -284,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 "" @@ -315,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 "" @@ -346,6 +356,9 @@ 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/form_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Required" msgstr "" @@ -403,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 "" @@ -669,6 +683,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 "Show in overview" msgstr "" @@ -1410,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" @@ -1438,3 +1457,61 @@ 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/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 "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 e2a1876..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 @@ -170,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 @@ -184,6 +186,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 +200,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 @@ -256,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 @@ -269,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 "" @@ -284,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 "" @@ -315,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 "" @@ -346,6 +356,9 @@ 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/form_component.ex +#: lib/mv_web/live/member_field_live/index_component.ex #, elixir-autogen, elixir-format msgid "Required" msgstr "" @@ -403,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 "" @@ -669,6 +683,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 "Show in overview" msgstr "" @@ -1410,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" @@ -1439,6 +1458,64 @@ 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/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, 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)" @@ -1456,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" @@ -1482,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" 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..e2e1be3 --- /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