@@ -789,6 +999,11 @@ defmodule MvWeb.CoreComponents do
>
Kernel.++(["focus-visible:ring-inset"])
+ |> Enum.join(" ")
+
+ "flex items-center gap-2 px-2 py-1 rounded cursor-pointer hover:bg-base-200 w-full text-left #{focus}"
+ end
+
@impl true
def mount(socket) do
{:ok, assign(socket, :open, false)}
@@ -59,7 +69,7 @@ defmodule MvWeb.Components.ExportDropdown do
@@ -75,7 +85,7 @@ defmodule MvWeb.Components.ExportDropdown do
diff --git a/lib/mv_web/helpers/membership_fee_helpers.ex b/lib/mv_web/helpers/membership_fee_helpers.ex
index 27c99f5..e8a2ce8 100644
--- a/lib/mv_web/helpers/membership_fee_helpers.ex
+++ b/lib/mv_web/helpers/membership_fee_helpers.ex
@@ -219,6 +219,17 @@ defmodule MvWeb.Helpers.MembershipFeeHelpers do
def status_color(:unpaid), do: "badge-error"
def status_color(:suspended), do: "badge-ghost"
+ @doc """
+ Returns the Core Components badge variant for a cycle status (WCAG-compliant).
+
+ Use with <.badge variant={MembershipFeeHelpers.status_variant(status)}>.
+ Suspended uses :warning (yellow) to match the edit cycle-status button.
+ """
+ @spec status_variant(:paid | :unpaid | :suspended) :: :success | :error | :warning
+ def status_variant(:paid), do: :success
+ def status_variant(:unpaid), do: :error
+ def status_variant(:suspended), do: :warning
+
@doc """
Gets the icon name for a status.
diff --git a/lib/mv_web/live/components/member_filter_component.ex b/lib/mv_web/live/components/member_filter_component.ex
index 4ee72d3..4a42bbc 100644
--- a/lib/mv_web/live/components/member_filter_component.ex
+++ b/lib/mv_web/live/components/member_filter_component.ex
@@ -58,8 +58,9 @@ defmodule MvWeb.Components.MemberFilterComponent do
def render(assigns) do
~H"""
-
0}
- class="badge badge-primary badge-sm"
+ variant="primary"
+ size="sm"
>
{active_boolean_filters_count(@boolean_filters)}
-
-
+ <.badge
:if={
(@cycle_status_filter || map_size(@group_filters) > 0) &&
active_boolean_filters_count(@boolean_filters) == 0
}
- class="badge badge-primary badge-sm"
+ variant="primary"
+ size="sm"
>
{@member_count}
-
+
-
-
-
-
- {if @user, do: gettext("Change Password"), else: gettext("Set Password")}
-
-
-
- <%= if @show_password_fields do %>
-
- <%= if @user && MvWeb.Helpers.UserHelpers.has_oidc?(@user) do %>
-
-
- {gettext("SSO / OIDC user")}
-
-
- {gettext(
- "This user is linked via SSO (Single Sign-On). A password set or changed here only affects login with email and password in this application. It does not change the password in your identity provider (e.g. Authentik). To change the SSO password, use the identity provider or your organization's IT."
- )}
-
-
- <% end %>
+ <%= if @user && @can_assign_role do %>
+
<.input
- field={@form[:password]}
- label={gettext("Password")}
- type="password"
- required
- autocomplete="new-password"
+ field={@form[:role_id]}
+ type="select"
+ label={gettext("Role")}
+ options={Enum.map(@roles, &{&1.name, &1.id})}
+ prompt={gettext("Select role...")}
/>
-
-
- <%= if !@user do %>
- <.input
- field={@form[:password_confirmation]}
- label={gettext("Confirm Password")}
- type="password"
- required
- autocomplete="new-password"
- />
- <% end %>
-
-
-
{gettext("Password requirements")}:
-
- {gettext("At least 8 characters")}
- {gettext("Include both letters and numbers")}
- {gettext("Consider using special characters")}
-
-
-
- <%= if @user && @can_manage_member_linking do %>
-
-
- {gettext("Admin Note")}: {gettext(
- "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system."
- )}
-
-
- <% end %>
- <% else %>
- <%= if @user do %>
-
-
- {gettext("Note")}: {gettext(
- "Check 'Change Password' above to set a new password for this user."
- )}
-
-
- <% else %>
-
-
- {gettext("Note")}: {gettext(
- "User will be created without a password. Check 'Set Password' to add one."
- )}
-
-
- <% end %>
<% end %>
-
-
-
- <%= if @can_manage_member_linking do %>
+
+
-
{gettext("Linked Member")}
+
+
+
+ {if @user, do: gettext("Change Password"), else: gettext("Set Password")}
+
+
- <%= if @user && @user.member && !@unlink_member do %>
-
-
-
-
-
- {MvWeb.Helpers.MemberHelpers.display_name(@user.member)}
+ <%= if @show_password_fields do %>
+
+ <%= if @user && MvWeb.Helpers.UserHelpers.has_oidc?(@user) do %>
+
+
+ {gettext("SSO / OIDC user")}
-
{@user.member.email}
-
- <.button
- type="button"
- variant="danger"
- size="sm"
- phx-click="unlink_member"
- >
- {gettext("Unlink Member")}
-
-
-
- <% else %>
- <%= if @unlink_member do %>
-
-
-
- {gettext("Unlinking scheduled")}: {gettext(
- "Member will be unlinked when you save. Cannot select new member until saved."
- )}
-
-
- <% end %>
-
-
-
-
-
- <%= if length(@available_members) > 0 do %>
-
- <%= for {member, index} <- Enum.with_index(@available_members) do %>
-
-
- {MvWeb.Helpers.MemberHelpers.display_name(member)}
-
-
{member.email}
-
- <% end %>
-
- <% end %>
-
-
- <%= if @user && @user.email && @available_members != [] && Enum.all?(@available_members, &(&1.email == to_string(@user.email))) do %>
-
-
- {gettext("Note")}: {gettext(
- "A member with this email already exists. To link with a different member, please change one of the email addresses first."
+
+ {gettext(
+ "This user is linked via SSO (Single Sign-On). A password set or changed here only affects login with email and password in this application. It does not change the password in your identity provider (e.g. Authentik). To change the SSO password, use the identity provider or your organization's IT."
)}
<% end %>
+ <.input
+ field={@form[:password]}
+ label={gettext("Password")}
+ type="password"
+ required
+ autocomplete="new-password"
+ />
+
+
+ <%= if !@user do %>
+ <.input
+ field={@form[:password_confirmation]}
+ label={gettext("Confirm Password")}
+ type="password"
+ required
+ autocomplete="new-password"
+ />
+ <% end %>
- <%= if @selected_member_id && @selected_member_name do %>
-
-
- {gettext("Selected")}: {@selected_member_name}
-
-
- {gettext("Save to confirm linking.")}
+
+
{gettext("Password requirements")}:
+
+ {gettext("At least 8 characters")}
+ {gettext("Include both letters and numbers")}
+ {gettext("Consider using special characters")}
+
+
+
+ <%= if @user && @can_manage_member_linking do %>
+
+
+ {gettext("Admin Note")}: {gettext(
+ "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system."
+ )}
<% end %>
+ <% else %>
+ <%= if @user do %>
+
+
+ {gettext("Note")}: {gettext(
+ "Check 'Change Password' above to set a new password for this user."
+ )}
+
+
+ <% else %>
+
+
+ {gettext("Note")}: {gettext(
+ "User will be created without a password. Check 'Set Password' to add one."
+ )}
+
+
+ <% end %>
<% end %>
- <% end %>
+
+
+ <%= if @can_manage_member_linking do %>
+
+
{gettext("Linked Member")}
- <%!-- Danger zone: canonical pattern (same as member form) --%>
- <%= if @user && can?(@current_user, :destroy, @user) && !Mv.Helpers.SystemActor.system_user?(@user) do %>
-
-
- {gettext("Danger zone")}
-
-
-
- {gettext(
- "Deleting this user cannot be undone. The user account and any linked member association will be affected."
- )}
-
- <.button
- type="button"
- variant="danger"
- phx-click="delete"
- phx-value-id={@user.id}
- data-confirm={
- gettext(
+ <%= if @user && @user.member && !@unlink_member do %>
+
+
+
+
+
+ {MvWeb.Helpers.MemberHelpers.display_name(@user.member)}
+
+
{@user.member.email}
+
+ <.button
+ type="button"
+ variant="danger"
+ size="sm"
+ phx-click="unlink_member"
+ >
+ {gettext("Unlink Member")}
+
+
+
+ <% else %>
+ <%= if @unlink_member do %>
+
+
+
+ {gettext("Unlinking scheduled")}: {gettext(
+ "Member will be unlinked when you save. Cannot select new member until saved."
+ )}
+
+
+ <% end %>
+
+
+
+
+
+ <%= if length(@available_members) > 0 do %>
+
+ <%= for {member, index} <- Enum.with_index(@available_members) do %>
+
+
+ {MvWeb.Helpers.MemberHelpers.display_name(member)}
+
+
{member.email}
+
+ <% end %>
+
+ <% end %>
+
+
+ <%= if @user && @user.email && @available_members != [] && Enum.all?(@available_members, &(&1.email == to_string(@user.email))) do %>
+
+
+ {gettext("Note")}: {gettext(
+ "A member with this email already exists. To link with a different member, please change one of the email addresses first."
+ )}
+
+
+ <% end %>
+
+ <%= if @selected_member_id && @selected_member_name do %>
+
+
+ {gettext("Selected")}: {@selected_member_name}
+
+
+ {gettext("Save to confirm linking.")}
+
+
+ <% end %>
+
+ <% end %>
+
+ <% end %>
+
+ <%!-- Danger zone: canonical pattern (same as member form) --%>
+ <%= if @user && can?(@current_user, :destroy, @user) && !Mv.Helpers.SystemActor.system_user?(@user) do %>
+
+
+ {gettext("Danger zone")}
+
+
+
+ {gettext(
+ "Deleting this user cannot be undone. The user account and any linked member association will be affected."
+ )}
+
+ <.button
+ id="delete-user-form-trigger"
+ type="button"
+ variant="danger"
+ phx-click="open_delete_modal"
+ data-testid="user-delete"
+ aria-label={gettext("Delete user %{email}", email: @user.email)}
+ >
+ <.icon name="hero-trash" class="size-4" />
+ {gettext("Delete user")}
+
+
+
+ <% end %>
+
+ <%!-- Delete User Confirmation Modal (WCAG: focus in modal, keyboard confirm/cancel) --%>
+ <%= if @user && assigns[:show_delete_modal] do %>
+
+
+
+ {gettext("Delete User")}
+
+
+ {gettext(
"Are you sure you want to delete the user %{email}? This action cannot be undone.",
email: @user.email
- )
- }
- data-testid="user-delete"
- aria-label={gettext("Delete user %{email}", email: @user.email)}
- >
- <.icon name="hero-trash" class="size-4" />
- {gettext("Delete user")}
-
-
-
- <% end %>
+ )}
+
+
+ <.button
+ type="button"
+ variant="neutral"
+ phx-click="cancel_delete_modal"
+ phx-mounted={JS.focus()}
+ id="delete-user-form-modal-cancel"
+ aria-label={gettext("Cancel")}
+ >
+ {gettext("Cancel")}
+
+ <.button
+ type="button"
+ variant="danger"
+ phx-click={JS.push("delete", value: %{id: @user.id})}
+ aria-label={gettext("Delete user")}
+ >
+ {gettext("Delete")}
+
+
+
+
+ <% end %>
-
- <.button navigate={return_path(@return_to, @user)} variant="neutral">
- {gettext("Cancel")}
-
- <.button phx-disable-with={gettext("Saving...")} variant="primary">
- {gettext("Save User")}
-
-
-
+
+ <.button navigate={return_path(@return_to, @user)} variant="neutral">
+ {gettext("Cancel")}
+
+ <.button phx-disable-with={gettext("Saving...")} variant="primary">
+ {gettext("Save User")}
+
+
+
+
"""
end
@@ -399,6 +441,7 @@ defmodule MvWeb.UserLive.Form do
|> assign(:selected_member_name, nil)
|> assign(:unlink_member, false)
|> assign(:focused_member_index, nil)
+ |> assign_new(:show_delete_modal, fn -> false end)
|> load_initial_members()
|> assign_form()}
end
@@ -454,6 +497,32 @@ defmodule MvWeb.UserLive.Form do
end
end
+ @impl true
+ def handle_event("open_delete_modal", _params, socket) do
+ {:noreply, assign(socket, :show_delete_modal, true)}
+ end
+
+ @impl true
+ def handle_event("cancel_delete_modal", _params, socket) do
+ {:noreply, close_delete_modal_and_restore_focus(socket)}
+ end
+
+ def handle_event("dialog_keydown", %{"key" => key}, socket) when key in ["Escape", "Esc"] do
+ {:noreply, close_delete_modal_and_restore_focus(socket)}
+ end
+
+ def handle_event("dialog_keydown", _params, socket), do: {:noreply, socket}
+
+ def handle_event("window_keydown", %{"key" => key}, socket) when key in ["Escape", "Esc"] do
+ if socket.assigns[:show_delete_modal] do
+ {:noreply, close_delete_modal_and_restore_focus(socket)}
+ else
+ {:noreply, socket}
+ end
+ end
+
+ def handle_event("window_keydown", _params, socket), do: {:noreply, socket}
+
@impl true
def handle_event("delete", %{"id" => id}, socket) do
user = socket.assigns.user
@@ -461,13 +530,22 @@ defmodule MvWeb.UserLive.Form do
cond do
is_nil(user) ->
- {:noreply, put_flash(socket, :error, gettext("User not found"))}
+ {:noreply,
+ socket
+ |> put_flash(:error, gettext("User not found"))
+ |> assign(:show_delete_modal, false)}
to_string(id) != to_string(user.id) ->
- {:noreply, put_flash(socket, :error, gettext("User not found"))}
+ {:noreply,
+ socket
+ |> put_flash(:error, gettext("User not found"))
+ |> assign(:show_delete_modal, false)}
Mv.Helpers.SystemActor.system_user?(user) ->
- {:noreply, put_flash(socket, :error, gettext("System user cannot be deleted."))}
+ {:noreply,
+ socket
+ |> put_flash(:error, gettext("System user cannot be deleted."))
+ |> assign(:show_delete_modal, false)}
true ->
handle_user_delete_destroy(socket, user, actor)
@@ -594,13 +672,24 @@ defmodule MvWeb.UserLive.Form do
{:error, %Ash.Error.Forbidden{}} ->
{:noreply,
- put_flash(socket, :error, gettext("You do not have permission to delete this user"))}
+ socket
+ |> put_flash(:error, gettext("You do not have permission to delete this user"))
+ |> assign(:show_delete_modal, false)}
{:error, error} ->
- {:noreply, put_flash(socket, :error, format_ash_error(error))}
+ {:noreply,
+ socket
+ |> put_flash(:error, format_ash_error(error))
+ |> assign(:show_delete_modal, false)}
end
end
+ defp close_delete_modal_and_restore_focus(socket) do
+ socket
+ |> assign(:show_delete_modal, false)
+ |> push_event("focus_restore", %{id: "delete-user-form-trigger"})
+ end
+
defp handle_member_linking(socket, user, actor) do
result = perform_member_link_action(socket, user, actor)
diff --git a/lib/mv_web/live/user_live/index.html.heex b/lib/mv_web/live/user_live/index.html.heex
index 86f0ab7..c84f258 100644
--- a/lib/mv_web/live/user_live/index.html.heex
+++ b/lib/mv_web/live/user_live/index.html.heex
@@ -1,6 +1,7 @@
<.header>
- {gettext("Listing Users")}
+ {gettext("Users")}
+ <:subtitle>{gettext("Manage users and their permissions.")}
<:actions>
<%= if can?(@current_user, :create, Mv.Accounts.User) do %>
<.button variant="primary" navigate={~p"/users/new"} data-testid="user-new">
@@ -37,25 +38,25 @@
{user.role.name}
<:col :let={user} label={gettext("Linked Member")}>
- <%= if user.member do %>
+ <.maybe_value value={user.member} empty_sr_text={gettext("No member linked")}>
{MvWeb.Helpers.MemberHelpers.display_name(user.member)}
- <% else %>
- {gettext("No member linked")}
- <% end %>
+
<:col :let={user} label={gettext("Password")}>
- <%= if MvWeb.Helpers.UserHelpers.has_password?(user) do %>
+ <.maybe_value
+ value={MvWeb.Helpers.UserHelpers.has_password?(user)}
+ empty_sr_text={gettext("Not set")}
+ >
{gettext("Enabled")}
- <% else %>
- —
- <% end %>
+
<:col :let={user} label={gettext("OIDC")}>
- <%= if MvWeb.Helpers.UserHelpers.has_oidc?(user) do %>
+ <.maybe_value
+ value={MvWeb.Helpers.UserHelpers.has_oidc?(user)}
+ empty_sr_text={gettext("Not set")}
+ >
{gettext("Linked")}
- <% else %>
- —
- <% end %>
+
diff --git a/lib/mv_web/live/user_live/show.ex b/lib/mv_web/live/user_live/show.ex
index d7a12b2..770be82 100644
--- a/lib/mv_web/live/user_live/show.ex
+++ b/lib/mv_web/live/user_live/show.ex
@@ -45,8 +45,6 @@ defmodule MvWeb.UserLive.Show do
{gettext("User")} {@user.email}
- <:subtitle>{gettext("This is a user record from your database.")}
-
<:actions>
<%= if can?(@current_user, :update, @user) do %>
<.button
@@ -60,65 +58,106 @@ defmodule MvWeb.UserLive.Show do
- <.list>
- <:item title={gettext("Email")}>{@user.email}
- <:item title={gettext("Role")}>{@user.role.name}
- <:item title={gettext("Password Authentication")}>
- {if MvWeb.Helpers.UserHelpers.has_password?(@user),
- do: gettext("Enabled"),
- else: gettext("Not enabled")}
-
- <:item title={gettext("OIDC")}>
- {if MvWeb.Helpers.UserHelpers.has_oidc?(@user),
- do: gettext("Linked"),
- else: gettext("Not linked")}
-
- <:item title={gettext("Linked Member")}>
- <%= if @user.member do %>
- <.link
- navigate={~p"/members/#{@user.member}"}
- class="text-blue-600 underline hover:text-blue-800"
- >
- <.icon name="hero-users" class="inline w-4 h-4 mr-1" />
- {MvWeb.Helpers.MemberHelpers.display_name(@user.member)}
-
- <% else %>
-
{gettext("No member linked")}
- <% end %>
-
-
+
+ <.list>
+ <:item title={gettext("Email")}>{@user.email}
+ <:item title={gettext("Role")}>{@user.role.name}
+ <:item title={gettext("Password Authentication")}>
+ {if MvWeb.Helpers.UserHelpers.has_password?(@user),
+ do: gettext("Enabled"),
+ else: gettext("Not enabled")}
+
+ <:item title={gettext("OIDC")}>
+ {if MvWeb.Helpers.UserHelpers.has_oidc?(@user),
+ do: gettext("Linked"),
+ else: gettext("Not linked")}
+
+ <:item title={gettext("Linked Member")}>
+ <%= if @user.member do %>
+ <.link
+ navigate={~p"/members/#{@user.member}"}
+ class="text-blue-600 underline hover:text-blue-800"
+ >
+ <.icon name="hero-users" class="inline w-4 h-4 mr-1" />
+ {MvWeb.Helpers.MemberHelpers.display_name(@user.member)}
+
+ <% else %>
+
{gettext("No member linked")}
+ <% end %>
+
+
- <%!-- Danger zone: canonical pattern (same as member show) --%>
- <%= if can?(@current_user, :destroy, @user) and not Mv.Helpers.SystemActor.system_user?(@user) do %>
-
-
- {gettext("Danger zone")}
-
-
-
- {gettext(
- "Deleting this user cannot be undone. The user account and any linked member association will be affected."
- )}
-
- <.button
- variant="danger"
- phx-click="delete"
- phx-value-id={@user.id}
- data-confirm={
- gettext(
+ <%!-- Danger zone: canonical pattern (same as member show) --%>
+ <%= if can?(@current_user, :destroy, @user) and not Mv.Helpers.SystemActor.system_user?(@user) do %>
+
+
+ {gettext("Danger zone")}
+
+
+
+ {gettext(
+ "Deleting this user cannot be undone. The user account and any linked member association will be affected."
+ )}
+
+ <.button
+ id="delete-user-trigger"
+ variant="danger"
+ phx-click="open_delete_modal"
+ data-testid="user-delete"
+ aria-label={gettext("Delete user %{email}", email: @user.email)}
+ >
+ <.icon name="hero-trash" class="size-4" />
+ {gettext("Delete user")}
+
+
+
+ <% end %>
+
+ <%!-- Delete User Confirmation Modal (WCAG: focus in modal, keyboard confirm/cancel) --%>
+ <%= if assigns[:show_delete_modal] do %>
+
+
+
{gettext("Delete User")}
+
+ {gettext(
"Are you sure you want to delete the user %{email}? This action cannot be undone.",
email: @user.email
- )
- }
- data-testid="user-delete"
- aria-label={gettext("Delete user %{email}", email: @user.email)}
- >
- <.icon name="hero-trash" class="size-4" />
- {gettext("Delete user")}
-
-
-
- <% end %>
+ )}
+
+
+ <.button
+ type="button"
+ variant="neutral"
+ phx-click="cancel_delete_modal"
+ phx-mounted={JS.focus()}
+ id="delete-user-modal-cancel"
+ aria-label={gettext("Cancel")}
+ >
+ {gettext("Cancel")}
+
+ <.button
+ type="button"
+ variant="danger"
+ phx-click={JS.push("delete", value: %{id: @user.id})}
+ aria-label={gettext("Delete user")}
+ >
+ {gettext("Delete")}
+
+
+
+
+ <% end %>
+
"""
end
@@ -139,10 +178,37 @@ defmodule MvWeb.UserLive.Show do
{:ok,
socket
|> assign(:page_title, gettext("Show User"))
- |> assign(:user, user)}
+ |> assign(:user, user)
+ |> assign(:show_delete_modal, false)}
end
end
+ @impl true
+ def handle_event("open_delete_modal", _params, socket) do
+ {:noreply, assign(socket, :show_delete_modal, true)}
+ end
+
+ @impl true
+ def handle_event("cancel_delete_modal", _params, socket) do
+ {:noreply, close_delete_modal_and_restore_focus(socket)}
+ end
+
+ def handle_event("window_keydown", %{"key" => key}, socket) when key in ["Escape", "Esc"] do
+ if socket.assigns[:show_delete_modal] do
+ {:noreply, close_delete_modal_and_restore_focus(socket)}
+ else
+ {:noreply, socket}
+ end
+ end
+
+ def handle_event("window_keydown", _params, socket), do: {:noreply, socket}
+
+ def handle_event("dialog_keydown", %{"key" => key}, socket) when key in ["Escape", "Esc"] do
+ {:noreply, close_delete_modal_and_restore_focus(socket)}
+ end
+
+ def handle_event("dialog_keydown", _params, socket), do: {:noreply, socket}
+
@impl true
def handle_event("delete", %{"id" => id}, socket) do
user = socket.assigns.user
@@ -150,10 +216,16 @@ defmodule MvWeb.UserLive.Show do
cond do
to_string(id) != to_string(user.id) ->
- {:noreply, put_flash(socket, :error, gettext("User not found"))}
+ {:noreply,
+ socket
+ |> put_flash(:error, gettext("User not found"))
+ |> assign(:show_delete_modal, false)}
Mv.Helpers.SystemActor.system_user?(user) ->
- {:noreply, put_flash(socket, :error, gettext("System user cannot be deleted."))}
+ {:noreply,
+ socket
+ |> put_flash(:error, gettext("System user cannot be deleted."))
+ |> assign(:show_delete_modal, false)}
true ->
handle_user_delete_destroy(socket, user, actor)
@@ -170,10 +242,21 @@ defmodule MvWeb.UserLive.Show do
{:error, %Ash.Error.Forbidden{}} ->
{:noreply,
- put_flash(socket, :error, gettext("You do not have permission to delete this user"))}
+ socket
+ |> put_flash(:error, gettext("You do not have permission to delete this user"))
+ |> assign(:show_delete_modal, false)}
{:error, error} ->
- {:noreply, put_flash(socket, :error, format_ash_error(error))}
+ {:noreply,
+ socket
+ |> put_flash(:error, format_ash_error(error))
+ |> assign(:show_delete_modal, false)}
end
end
+
+ defp close_delete_modal_and_restore_focus(socket) do
+ socket
+ |> assign(:show_delete_modal, false)
+ |> push_event("focus_restore", %{id: "delete-user-trigger"})
+ end
end
diff --git a/lib/mv_web/member_live/index/membership_fee_status.ex b/lib/mv_web/member_live/index/membership_fee_status.ex
index bf4dd53..d586b28 100644
--- a/lib/mv_web/member_live/index/membership_fee_status.ex
+++ b/lib/mv_web/member_live/index/membership_fee_status.ex
@@ -93,22 +93,30 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatus do
## Returns
- Map with `:color`, `:icon`, and `:label` keys, or `nil` if status is nil
+ Map with `:variant`, `:icon`, and `:label` keys (and legacy `:color`), or `nil` if status is nil.
+ Use `:variant` with <.badge variant={badge.variant}> for WCAG-compliant rendering.
## Examples
iex> MvWeb.MemberLive.Index.MembershipFeeStatus.format_cycle_status_badge(:paid)
- %{color: "badge-success", icon: "hero-check-circle", label: "Paid"}
+ %{variant: :success, color: "badge-success", icon: "hero-check-circle", label: "Paid"}
iex> MvWeb.MemberLive.Index.MembershipFeeStatus.format_cycle_status_badge(nil)
nil
"""
@spec format_cycle_status_badge(:paid | :unpaid | :suspended | nil) ::
- %{color: String.t(), icon: String.t(), label: String.t()} | nil
+ %{
+ variant: :success | :error | :warning,
+ color: String.t(),
+ icon: String.t(),
+ label: String.t()
+ }
+ | nil
def format_cycle_status_badge(nil), do: nil
def format_cycle_status_badge(status) when status in [:paid, :unpaid, :suspended] do
%{
+ variant: MembershipFeeHelpers.status_variant(status),
color: MembershipFeeHelpers.status_color(status),
icon: MembershipFeeHelpers.status_icon(status),
label: format_status_label(status)
diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po
index 2f3a1f8..32409ea 100644
--- a/priv/gettext/de/LC_MESSAGES/default.po
+++ b/priv/gettext/de/LC_MESSAGES/default.po
@@ -36,7 +36,12 @@ msgid "City"
msgstr "Stadt"
#: lib/mv_web/live/group_live/show.ex
+#: lib/mv_web/live/member_live/form.ex
+#: lib/mv_web/live/member_live/show.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#: lib/mv_web/live/role_live/show.ex
+#: lib/mv_web/live/user_live/form.ex
+#: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format
msgid "Delete"
msgstr "Löschen"
@@ -257,9 +262,12 @@ msgstr "Dein Passwort wurde erfolgreich zurückgesetzt"
#: lib/mv_web/live/group_live/show.ex
#: lib/mv_web/live/member_field_live/form_component.ex
#: lib/mv_web/live/member_live/form.ex
+#: lib/mv_web/live/member_live/show.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex
+#: lib/mv_web/live/role_live/show.ex
#: lib/mv_web/live/user_live/form.ex
+#: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format
msgid "Cancel"
msgstr "Abbrechen"
@@ -294,7 +302,6 @@ msgid "Logout"
msgstr "Abmelden"
#: lib/mv_web/live/user_live/index.ex
-#: lib/mv_web/live/user_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Listing Users"
msgstr "Benutzer*innen auflisten"
@@ -381,16 +388,6 @@ msgstr "Benutzer*in speichern"
msgid "Show User"
msgstr "Benutzer*in anzeigen"
-#: lib/mv_web/live/user_live/show.ex
-#, elixir-autogen, elixir-format
-msgid "This is a user record from your database."
-msgstr "Dies ist ein Benutzer*innen-Datensatz aus Ihrer Datenbank."
-
-#: lib/mv_web/live/user_live/form.ex
-#, elixir-autogen, elixir-format
-msgid "Use this form to manage user records in your database."
-msgstr "Verwende dieses Formular, um Benutzer*innen-Datensätze zu verwalten."
-
#: lib/mv_web/live/user_live/form.ex
#: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format
@@ -533,6 +530,7 @@ msgstr "Suchen..."
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/role_live/index.html.heex
+#: lib/mv_web/live/user_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Users"
msgstr "Benutzer*innen"
@@ -593,18 +591,6 @@ msgstr "Diese E-Mail-Adresse ist bereits mit einem anderen OIDC-Konto verknüpft
msgid "Custom Fields"
msgstr "Benutzerdefinierte Felder"
-#: lib/mv_web/live/custom_field_live/index_component.ex
-#, elixir-autogen, elixir-format
-msgid "%{count} member has a value assigned for this custom field."
-msgid_plural "%{count} members have values assigned for this custom field."
-msgstr[0] "%{count} Mitglied hat einen Wert für dieses benutzerdefinierte Feld zugewiesen."
-msgstr[1] "%{count} Mitglieder haben Werte für dieses benutzerdefinierte Feld zugewiesen."
-
-#: lib/mv_web/live/custom_field_live/index_component.ex
-#, elixir-autogen, elixir-format
-msgid "All custom field values will be permanently deleted when you delete this custom field."
-msgstr "Alle benutzerdefinierten Feldwerte werden beim Löschen dieses benutzerdefinierten Feldes dauerhaft gelöscht."
-
#: lib/mv_web/live/custom_field_live/index_component.ex
#, elixir-autogen, elixir-format
msgid "Enter the text above to confirm"
@@ -790,6 +776,7 @@ msgstr "Beitragsdaten"
msgid "Payments"
msgstr "Zahlungen"
+#: lib/mv_web/live/datafields_live.ex
#: lib/mv_web/live/member_live/form.ex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format
@@ -1388,6 +1375,8 @@ msgid "None (no default)"
msgstr "Keine (kein Standard)"
#: lib/mv_web/live/member_live/show.ex
+#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#: lib/mv_web/live/user_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Not set"
msgstr "Nicht gesetzt"
@@ -1473,11 +1462,6 @@ msgstr "Art"
msgid "Type '%{confirmation}' to confirm"
msgstr "Gib '%{confirmation}' ein, um zu bestätigen"
-#: lib/mv_web/live/membership_fee_type_live/form.ex
-#, elixir-autogen, elixir-format
-msgid "Use this form to manage membership fee types in your database."
-msgstr "Verwende dieses Formular, um Mitgliedsbeitragsarten in deiner Datenbank zu verwalten."
-
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format
msgid "Warning"
@@ -1704,11 +1688,6 @@ msgstr "System-Rollen können nicht gelöscht werden."
msgid "Toggle sidebar"
msgstr "Sidebar umschalten"
-#: lib/mv_web/live/role_live/form.ex
-#, elixir-autogen, elixir-format
-msgid "Use this form to manage roles in your database."
-msgstr "Verwende dieses Formular, um Rollen in deiner Datenbank zu verwalten."
-
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "User menu"
@@ -2428,11 +2407,6 @@ msgstr "Alle Jahre zusammengefasst (Kreis)"
msgid "Contributions by year"
msgstr "Beiträge nach Jahr"
-#: lib/mv_web/live/statistics_live.ex
-#, elixir-autogen, elixir-format
-msgid "Overview from first membership to today"
-msgstr "Übersicht vom ersten Eintritt bis heute"
-
#: lib/mv_web/live/statistics_live.ex
#, elixir-autogen, elixir-format
msgid "Contributions by year as table with stacked bars"
@@ -2910,11 +2884,6 @@ msgstr "CSV Datei auswählen"
msgid "Import Members"
msgstr "Mitglieder importieren (CSV)"
-#~ #: lib/mv_web/live/import_live.ex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "Datei auswählen"
-#~ msgstr ""
-
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Admin group name"
@@ -2940,21 +2909,6 @@ msgstr "Client-ID"
msgid "Client Secret"
msgstr "Client-Geheimnis"
-#: lib/mv_web/live/membership_fee_settings_live.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Configure global settings and fee types for membership fees."
-msgstr "Globale Einstellungen für Mitgliedsbeiträge konfigurieren."
-
-#: lib/mv_web/live/datafields_live.ex
-#, elixir-autogen, elixir-format
-msgid "Configure member fields and custom data fields."
-msgstr "Mitgliedsfelder und benutzerdefinierte Datenfelder konfigurieren."
-
-#: lib/mv_web/live/datafields_live.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Custom fields"
-msgstr "Benutzerdefinierte Felder"
-
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
@@ -2996,11 +2950,6 @@ msgstr "Aus OIDC_REDIRECT_URI"
msgid "Groups claim"
msgstr "Gruppenclaim"
-#: lib/mv_web/live/datafields_live.ex
-#, elixir-autogen, elixir-format
-msgid "Member fields"
-msgstr "Mitgliedsfelder"
-
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Membership fee settings"
@@ -3225,88 +3174,64 @@ msgstr "Das Löschen dieses Datenfeldes kann nicht rückgängig gemacht werden.
msgid "Individual datafields"
msgstr "Individuelle Datenfelder"
-#~ #: lib/mv_web/live/member_field_live/form_component.ex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "Back to Settings"
-#~ msgstr "Zurück zu den Einstellungen"
+#: lib/mv_web/live/member_live/form.ex
+#: lib/mv_web/live/member_live/show.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Delete Member"
+msgstr "Mitglied löschen"
-#~ #: lib/mv_web/live/role_live/index.html.heex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "Cannot delete system role"
-#~ msgstr "System-Rolle kann nicht gelöscht werden"
+#: lib/mv_web/live/role_live/show.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Delete Role"
+msgstr "Rolle löschen"
-#~ #: lib/mv_web/live/custom_field_live/index_component.ex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "Click for custom field details"
-#~ msgstr "Klicke für Datenfeld-Details"
+#: lib/mv_web/live/user_live/form.ex
+#: lib/mv_web/live/user_live/show.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Delete User"
+msgstr "Benutzer*in löschen"
-#~ #: lib/mv_web/live/member_field_live/index_component.ex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "Click for datafield details"
-#~ msgstr "Klicke für Datenfeld-Details"
+#: lib/mv_web/live/membership_fee_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Configure fee types for membership fees."
+msgstr "Verwalte Beitragsarten und Mitgliedsbeiträge."
-#~ #: lib/mv_web/live/member_live/form.ex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "Coming soon"
-#~ msgstr "Demnächst verfügbar"
+#: lib/mv_web/live/datafields_live.ex
+#, elixir-autogen, elixir-format
+msgid "Configure which data you want to save for your members. Define individual datafields."
+msgstr "Verwalte welche Daten du für eure Mitglieder speichern möchtest. Lege individuelle datenfelder an."
-#~ #: lib/mv_web/live/components/field_visibility_dropdown_component.ex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "Custom Field %{id}"
-#~ msgstr "Benutzerdefiniertes Feld %{id}"
+#: lib/mv_web/live/user_live/index.html.heex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Manage users and their permissions."
+msgstr "Verwalte Benutzer*innen und ihre Berechtigungen."
-#~ #: 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 "Edit datafield"
-#~ msgstr "Datenfeld bearbeiten"
+#: lib/mv_web/live/custom_field_live/index_component.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "%{count} member has a value assigned for this datafield."
+msgid_plural "%{count} members have values assigned for this datafield."
+msgstr[0] "%{count} Mitglied hat einen Wert für dieses benutzerdefinierte Feld zugewiesen."
+msgstr[1] "%{count} Mitglieder haben Werte für dieses benutzerdefinierte Feld zugewiesen."
-#~ #: lib/mv_web/live/user_live/index.html.heex
-#~ #, elixir-autogen, elixir-format, fuzzy
-#~ msgid "Edit user"
-#~ msgstr "Benutzer*in bearbeiten"
+#: lib/mv_web/live/datafields_live.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Individual Datafields"
+msgstr "Individuelle Datenfelder"
-#~ #: lib/mv_web/live/components/member_filter_component.ex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "Reset"
-#~ msgstr "Zurücksetzen"
+#: lib/mv_web/live/member_live/index.html.heex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "No group assignment"
+msgstr "Keine Gruppenzuordnung"
-#~ #: lib/mv_web/live/role_live/show.ex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "Rolle bearbeiten"
-#~ msgstr "Rolle bearbeiten"
+#: lib/mv_web/components/core_components.ex
+#: lib/mv_web/live/group_live/index.ex
+#: lib/mv_web/live/member_live/index.html.heex
+#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#, elixir-autogen, elixir-format
+msgid "Not specified"
+msgstr "Nicht angegeben"
-#~ #: lib/mv_web/live/role_live/form.ex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "Save Role"
-#~ msgstr "Rolle speichern"
-
-#~ #: lib/mv_web/live/user_live/index.html.heex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "Select all users"
-#~ msgstr "Alle Benutzer*innen auswählen"
-
-#~ #: lib/mv_web/live/user_live/index.html.heex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "Select user"
-#~ msgstr "Benutzer*in auswählen"
-
-#~ #: lib/mv_web/live/role_live/index.html.heex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "System roles cannot be deleted"
-#~ msgstr "System-Rollen können nicht gelöscht werden"
-
-#~ #: lib/mv_web/live/group_live/index.ex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "View"
-#~ msgstr "Anzeigen"
-
-#~ #: lib/mv_web/live/member_live/index.ex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "You do not have permission to access this member"
-#~ msgstr "Du hast keine Berechtigung, auf dieses Mitglied zuzugreifen"
-
-#~ #: lib/mv_web/live/user_live/index.ex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "You do not have permission to access this user"
-#~ msgstr "Du hast keine Berechtigung, auf diese*n Benutzer*in zuzugreifen"
+#: lib/mv_web/live/custom_field_live/index_component.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "All datafield values will be permanently deleted when you delete this datafield."
+msgstr "Alle benutzerdefinierten Feldwerte werden beim Löschen dieses benutzerdefinierten Feldes dauerhaft gelöscht."
diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot
index 7413a20..6b80e27 100644
--- a/priv/gettext/default.pot
+++ b/priv/gettext/default.pot
@@ -37,7 +37,12 @@ msgid "City"
msgstr ""
#: lib/mv_web/live/group_live/show.ex
+#: lib/mv_web/live/member_live/form.ex
+#: lib/mv_web/live/member_live/show.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#: lib/mv_web/live/role_live/show.ex
+#: lib/mv_web/live/user_live/form.ex
+#: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format
msgid "Delete"
msgstr ""
@@ -258,9 +263,12 @@ msgstr ""
#: lib/mv_web/live/group_live/show.ex
#: lib/mv_web/live/member_field_live/form_component.ex
#: lib/mv_web/live/member_live/form.ex
+#: lib/mv_web/live/member_live/show.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex
+#: lib/mv_web/live/role_live/show.ex
#: lib/mv_web/live/user_live/form.ex
+#: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format
msgid "Cancel"
msgstr ""
@@ -295,7 +303,6 @@ msgid "Logout"
msgstr ""
#: lib/mv_web/live/user_live/index.ex
-#: lib/mv_web/live/user_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Listing Users"
msgstr ""
@@ -382,16 +389,6 @@ msgstr ""
msgid "Show User"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex
-#, elixir-autogen, elixir-format
-msgid "This is a user record from your database."
-msgstr ""
-
-#: lib/mv_web/live/user_live/form.ex
-#, elixir-autogen, elixir-format
-msgid "Use this form to manage user records in your database."
-msgstr ""
-
#: lib/mv_web/live/user_live/form.ex
#: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format
@@ -534,6 +531,7 @@ msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/role_live/index.html.heex
+#: lib/mv_web/live/user_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Users"
msgstr ""
@@ -594,18 +592,6 @@ msgstr ""
msgid "Custom Fields"
msgstr ""
-#: lib/mv_web/live/custom_field_live/index_component.ex
-#, elixir-autogen, elixir-format
-msgid "%{count} member has a value assigned for this custom field."
-msgid_plural "%{count} members have values assigned for this custom field."
-msgstr[0] ""
-msgstr[1] ""
-
-#: lib/mv_web/live/custom_field_live/index_component.ex
-#, elixir-autogen, elixir-format
-msgid "All custom field values will be permanently deleted when you delete this custom field."
-msgstr ""
-
#: lib/mv_web/live/custom_field_live/index_component.ex
#, elixir-autogen, elixir-format
msgid "Enter the text above to confirm"
@@ -791,6 +777,7 @@ msgstr ""
msgid "Payments"
msgstr ""
+#: lib/mv_web/live/datafields_live.ex
#: lib/mv_web/live/member_live/form.ex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format
@@ -1389,6 +1376,8 @@ msgid "None (no default)"
msgstr ""
#: lib/mv_web/live/member_live/show.ex
+#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#: lib/mv_web/live/user_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Not set"
msgstr ""
@@ -1474,11 +1463,6 @@ msgstr ""
msgid "Type '%{confirmation}' to confirm"
msgstr ""
-#: lib/mv_web/live/membership_fee_type_live/form.ex
-#, elixir-autogen, elixir-format
-msgid "Use this form to manage membership fee types in your database."
-msgstr ""
-
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format
msgid "Warning"
@@ -1705,11 +1689,6 @@ msgstr ""
msgid "Toggle sidebar"
msgstr ""
-#: lib/mv_web/live/role_live/form.ex
-#, elixir-autogen, elixir-format
-msgid "Use this form to manage roles in your database."
-msgstr ""
-
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "User menu"
@@ -2429,11 +2408,6 @@ msgstr ""
msgid "Contributions by year"
msgstr ""
-#: lib/mv_web/live/statistics_live.ex
-#, elixir-autogen, elixir-format
-msgid "Overview from first membership to today"
-msgstr ""
-
#: lib/mv_web/live/statistics_live.ex
#, elixir-autogen, elixir-format
msgid "Contributions by year as table with stacked bars"
@@ -2935,21 +2909,6 @@ msgstr ""
msgid "Client Secret"
msgstr ""
-#: lib/mv_web/live/membership_fee_settings_live.ex
-#, elixir-autogen, elixir-format
-msgid "Configure global settings and fee types for membership fees."
-msgstr ""
-
-#: lib/mv_web/live/datafields_live.ex
-#, elixir-autogen, elixir-format
-msgid "Configure member fields and custom data fields."
-msgstr ""
-
-#: lib/mv_web/live/datafields_live.ex
-#, elixir-autogen, elixir-format
-msgid "Custom fields"
-msgstr ""
-
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
@@ -2991,11 +2950,6 @@ msgstr ""
msgid "Groups claim"
msgstr ""
-#: lib/mv_web/live/datafields_live.ex
-#, elixir-autogen, elixir-format
-msgid "Member fields"
-msgstr ""
-
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Membership fee settings"
@@ -3219,3 +3173,65 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "Individual datafields"
msgstr ""
+
+#: lib/mv_web/live/member_live/form.ex
+#: lib/mv_web/live/member_live/show.ex
+#, elixir-autogen, elixir-format
+msgid "Delete Member"
+msgstr ""
+
+#: lib/mv_web/live/role_live/show.ex
+#, elixir-autogen, elixir-format
+msgid "Delete Role"
+msgstr ""
+
+#: lib/mv_web/live/user_live/form.ex
+#: lib/mv_web/live/user_live/show.ex
+#, elixir-autogen, elixir-format
+msgid "Delete User"
+msgstr ""
+
+#: lib/mv_web/live/membership_fee_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Configure fee types for membership fees."
+msgstr ""
+
+#: lib/mv_web/live/datafields_live.ex
+#, elixir-autogen, elixir-format
+msgid "Configure which data you want to save for your members. Define individual datafields."
+msgstr ""
+
+#: lib/mv_web/live/user_live/index.html.heex
+#, elixir-autogen, elixir-format
+msgid "Manage users and their permissions."
+msgstr ""
+
+#: lib/mv_web/live/custom_field_live/index_component.ex
+#, elixir-autogen, elixir-format
+msgid "%{count} member has a value assigned for this datafield."
+msgid_plural "%{count} members have values assigned for this datafield."
+msgstr[0] ""
+msgstr[1] ""
+
+#: lib/mv_web/live/datafields_live.ex
+#, elixir-autogen, elixir-format
+msgid "Individual Datafields"
+msgstr ""
+
+#: lib/mv_web/live/member_live/index.html.heex
+#, elixir-autogen, elixir-format
+msgid "No group assignment"
+msgstr ""
+
+#: lib/mv_web/components/core_components.ex
+#: lib/mv_web/live/group_live/index.ex
+#: lib/mv_web/live/member_live/index.html.heex
+#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#, elixir-autogen, elixir-format
+msgid "Not specified"
+msgstr ""
+
+#: lib/mv_web/live/custom_field_live/index_component.ex
+#, elixir-autogen, elixir-format
+msgid "All datafield values will be permanently deleted when you delete this datafield."
+msgstr ""
diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po
index 51aab4f..2ea6be5 100644
--- a/priv/gettext/en/LC_MESSAGES/default.po
+++ b/priv/gettext/en/LC_MESSAGES/default.po
@@ -37,7 +37,12 @@ msgid "City"
msgstr ""
#: lib/mv_web/live/group_live/show.ex
+#: lib/mv_web/live/member_live/form.ex
+#: lib/mv_web/live/member_live/show.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#: lib/mv_web/live/role_live/show.ex
+#: lib/mv_web/live/user_live/form.ex
+#: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format
msgid "Delete"
msgstr ""
@@ -258,9 +263,12 @@ msgstr ""
#: lib/mv_web/live/group_live/show.ex
#: lib/mv_web/live/member_field_live/form_component.ex
#: lib/mv_web/live/member_live/form.ex
+#: lib/mv_web/live/member_live/show.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex
+#: lib/mv_web/live/role_live/show.ex
#: lib/mv_web/live/user_live/form.ex
+#: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format
msgid "Cancel"
msgstr ""
@@ -295,7 +303,6 @@ msgid "Logout"
msgstr ""
#: lib/mv_web/live/user_live/index.ex
-#: lib/mv_web/live/user_live/index.html.heex
#, elixir-autogen, elixir-format, fuzzy
msgid "Listing Users"
msgstr ""
@@ -382,16 +389,6 @@ msgstr ""
msgid "Show User"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "This is a user record from your database."
-msgstr ""
-
-#: lib/mv_web/live/user_live/form.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Use this form to manage user records in your database."
-msgstr ""
-
#: lib/mv_web/live/user_live/form.ex
#: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format
@@ -534,6 +531,7 @@ msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/role_live/index.html.heex
+#: lib/mv_web/live/user_live/index.html.heex
#, elixir-autogen, elixir-format, fuzzy
msgid "Users"
msgstr ""
@@ -594,18 +592,6 @@ msgstr ""
msgid "Custom Fields"
msgstr ""
-#: lib/mv_web/live/custom_field_live/index_component.ex
-#, elixir-autogen, elixir-format
-msgid "%{count} member has a value assigned for this custom field."
-msgid_plural "%{count} members have values assigned for this custom field."
-msgstr[0] ""
-msgstr[1] ""
-
-#: lib/mv_web/live/custom_field_live/index_component.ex
-#, elixir-autogen, elixir-format
-msgid "All custom field values will be permanently deleted when you delete this custom field."
-msgstr ""
-
#: lib/mv_web/live/custom_field_live/index_component.ex
#, elixir-autogen, elixir-format
msgid "Enter the text above to confirm"
@@ -791,6 +777,7 @@ msgstr ""
msgid "Payments"
msgstr ""
+#: lib/mv_web/live/datafields_live.ex
#: lib/mv_web/live/member_live/form.ex
#: lib/mv_web/live/member_live/show.ex
#, elixir-autogen, elixir-format
@@ -1389,6 +1376,8 @@ msgid "None (no default)"
msgstr ""
#: lib/mv_web/live/member_live/show.ex
+#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#: lib/mv_web/live/user_live/index.html.heex
#, elixir-autogen, elixir-format, fuzzy
msgid "Not set"
msgstr ""
@@ -1474,11 +1463,6 @@ msgstr ""
msgid "Type '%{confirmation}' to confirm"
msgstr ""
-#: lib/mv_web/live/membership_fee_type_live/form.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Use this form to manage membership fee types in your database."
-msgstr ""
-
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format
msgid "Warning"
@@ -1705,11 +1689,6 @@ msgstr ""
msgid "Toggle sidebar"
msgstr ""
-#: lib/mv_web/live/role_live/form.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Use this form to manage roles in your database."
-msgstr ""
-
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "User menu"
@@ -2429,11 +2408,6 @@ msgstr ""
msgid "Contributions by year"
msgstr ""
-#: lib/mv_web/live/statistics_live.ex
-#, elixir-autogen, elixir-format
-msgid "Overview from first membership to today"
-msgstr ""
-
#: lib/mv_web/live/statistics_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Contributions by year as table with stacked bars"
@@ -2935,21 +2909,6 @@ msgstr ""
msgid "Client Secret"
msgstr ""
-#: lib/mv_web/live/membership_fee_settings_live.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Configure global settings and fee types for membership fees."
-msgstr ""
-
-#: lib/mv_web/live/datafields_live.ex
-#, elixir-autogen, elixir-format
-msgid "Configure member fields and custom data fields."
-msgstr ""
-
-#: lib/mv_web/live/datafields_live.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Custom fields"
-msgstr ""
-
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
@@ -2991,11 +2950,6 @@ msgstr ""
msgid "Groups claim"
msgstr ""
-#: lib/mv_web/live/datafields_live.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Member fields"
-msgstr ""
-
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Membership fee settings"
@@ -3220,88 +3174,64 @@ msgstr ""
msgid "Individual datafields"
msgstr ""
-#~ #: lib/mv_web/live/member_field_live/form_component.ex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "Back to Settings"
-#~ msgstr ""
+#: lib/mv_web/live/member_live/form.ex
+#: lib/mv_web/live/member_live/show.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Delete Member"
+msgstr ""
-#~ #: lib/mv_web/live/role_live/index.html.heex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "Cannot delete system role"
-#~ msgstr ""
+#: lib/mv_web/live/role_live/show.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Delete Role"
+msgstr ""
-#~ #: lib/mv_web/live/custom_field_live/index_component.ex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "Click for custom field details"
-#~ msgstr ""
+#: lib/mv_web/live/user_live/form.ex
+#: lib/mv_web/live/user_live/show.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Delete User"
+msgstr ""
-#~ #: lib/mv_web/live/member_field_live/index_component.ex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "Click for datafield details"
-#~ msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Configure fee types for membership fees."
+msgstr ""
-#~ #: lib/mv_web/live/member_live/form.ex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "Coming soon"
-#~ msgstr ""
+#: lib/mv_web/live/datafields_live.ex
+#, elixir-autogen, elixir-format
+msgid "Configure which data you want to save for your members. Define individual datafields."
+msgstr ""
-#~ #: lib/mv_web/live/components/field_visibility_dropdown_component.ex
-#~ #, elixir-autogen, elixir-format, fuzzy
-#~ msgid "Custom Field %{id}"
-#~ msgstr ""
+#: lib/mv_web/live/user_live/index.html.heex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Manage users and their permissions."
+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 "Edit datafield"
-#~ msgstr ""
+#: lib/mv_web/live/custom_field_live/index_component.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "%{count} member has a value assigned for this datafield."
+msgid_plural "%{count} members have values assigned for this datafield."
+msgstr[0] ""
+msgstr[1] ""
-#~ #: lib/mv_web/live/user_live/index.html.heex
-#~ #, elixir-autogen, elixir-format, fuzzy
-#~ msgid "Edit user"
-#~ msgstr ""
+#: lib/mv_web/live/datafields_live.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Individual Datafields"
+msgstr ""
-#~ #: lib/mv_web/live/components/member_filter_component.ex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "Reset"
-#~ msgstr ""
+#: lib/mv_web/live/member_live/index.html.heex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "No group assignment"
+msgstr ""
-#~ #: lib/mv_web/live/role_live/show.ex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "Rolle bearbeiten"
-#~ msgstr ""
+#: lib/mv_web/components/core_components.ex
+#: lib/mv_web/live/group_live/index.ex
+#: lib/mv_web/live/member_live/index.html.heex
+#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#, elixir-autogen, elixir-format
+msgid "Not specified"
+msgstr ""
-#~ #: lib/mv_web/live/role_live/form.ex
-#~ #, elixir-autogen, elixir-format, fuzzy
-#~ msgid "Save Role"
-#~ msgstr ""
-
-#~ #: lib/mv_web/live/user_live/index.html.heex
-#~ #, elixir-autogen, elixir-format, fuzzy
-#~ msgid "Select all users"
-#~ msgstr ""
-
-#~ #: lib/mv_web/live/user_live/index.html.heex
-#~ #, elixir-autogen, elixir-format, fuzzy
-#~ msgid "Select user"
-#~ msgstr ""
-
-#~ #: lib/mv_web/live/role_live/index.html.heex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "System roles cannot be deleted"
-#~ msgstr ""
-
-#~ #: lib/mv_web/live/group_live/index.ex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "View"
-#~ msgstr ""
-
-#~ #: lib/mv_web/live/member_live/index.ex
-#~ #, elixir-autogen, elixir-format
-#~ msgid "You do not have permission to access this member"
-#~ msgstr ""
-
-#~ #: lib/mv_web/live/user_live/index.ex
-#~ #, elixir-autogen, elixir-format, fuzzy
-#~ msgid "You do not have permission to access this user"
-#~ msgstr ""
+#: lib/mv_web/live/custom_field_live/index_component.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "All datafield values will be permanently deleted when you delete this datafield."
+msgstr ""
diff --git a/test/mv_web/components/core_components_badge_test.exs b/test/mv_web/components/core_components_badge_test.exs
new file mode 100644
index 0000000..717dde6
--- /dev/null
+++ b/test/mv_web/components/core_components_badge_test.exs
@@ -0,0 +1,91 @@
+defmodule MvWeb.Components.CoreComponentsBadgeTest do
+ @moduledoc """
+ Unit tests for the Core Components badge (WCAG-compliant, non-transparent).
+ """
+ use MvWeb.ConnCase, async: true
+
+ import Phoenix.Component
+ import Phoenix.LiveViewTest
+ import MvWeb.CoreComponents
+
+ describe "badge/1" do
+ test "default variant renders with badge and badge-neutral classes (visible, not ghost)" do
+ assigns = %{}
+
+ html =
+ rendered_to_string(~H"""
+ <.badge variant="neutral">Label
+ """)
+
+ assert html =~ "badge"
+ assert html =~ "badge-neutral"
+ assert html =~ "badge-soft"
+ refute html =~ "badge-ghost"
+ assert html =~ "Label"
+ end
+
+ test "success variant renders badge-success and badge-soft" do
+ assigns = %{}
+
+ html =
+ rendered_to_string(~H"""
+ <.badge variant="success">Paid
+ """)
+
+ assert html =~ "badge-success"
+ assert html =~ "badge-soft"
+ assert html =~ "Paid"
+ end
+
+ test "outline style includes bg-base-100 for contrast" do
+ assigns = %{}
+
+ html =
+ rendered_to_string(~H"""
+ <.badge variant="primary" style="outline">Outline
+ """)
+
+ assert html =~ "badge-outline"
+ assert html =~ "bg-base-100"
+ assert html =~ "Outline"
+ end
+
+ test "solid style has no badge-soft or badge-outline" do
+ assigns = %{}
+
+ html =
+ rendered_to_string(~H"""
+ <.badge variant="error" style="solid">Error
+ """)
+
+ assert html =~ "badge-error"
+ refute html =~ "badge-soft"
+ refute html =~ "badge-outline"
+ assert html =~ "Error"
+ end
+
+ test "size sm adds badge-sm" do
+ assigns = %{}
+
+ html =
+ rendered_to_string(~H"""
+ <.badge variant="neutral" size="sm">Small
+ """)
+
+ assert html =~ "badge-sm"
+ assert html =~ "Small"
+ end
+
+ test "renders as span (non-interactive)" do
+ assigns = %{}
+
+ html =
+ rendered_to_string(~H"""
+ <.badge variant="info">Info
+ """)
+
+ assert html =~ ~r/
]*class="[^"]*badge[^"]*"/
+ refute html =~ ~r/ (suspended uses warning to match edit button)" do
+ assert MembershipFeeHelpers.status_variant(:paid) == :success
+ assert MembershipFeeHelpers.status_variant(:unpaid) == :error
+ assert MembershipFeeHelpers.status_variant(:suspended) == :warning
+ end
+ end
+
describe "status_color/1" do
test "returns correct color classes for statuses" do
assert MembershipFeeHelpers.status_color(:paid) == "badge-success"
diff --git a/test/mv_web/live/custom_field_live/deletion_test.exs b/test/mv_web/live/custom_field_live/deletion_test.exs
index 5ec955e..962ada1 100644
--- a/test/mv_web/live/custom_field_live/deletion_test.exs
+++ b/test/mv_web/live/custom_field_live/deletion_test.exs
@@ -46,15 +46,19 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
%{conn: conn, user: user_with_role}
end
- # Delete is in the edit form (FormComponent); open form by clicking the name cell (unique td with phx-click)
+ # Delete is in the edit form (FormComponent). First row click opens form (overview) or switches
+ # to edit-mode (new component shows table). If delete button is visible, click it; else click row
+ # again to open the form, then click delete.
defp open_delete_modal(view, custom_field) do
- view
- |> element("tr#custom_fields-#{custom_field.id} td", custom_field.name)
- |> render_click()
+ row_selector = "tr#custom_fields-#{custom_field.id} td"
+ view |> element(row_selector, custom_field.name) |> render_click()
- view
- |> element("[data-testid=custom-field-delete]")
- |> render_click()
+ if has_element?(view, "[data-testid=custom-field-delete]") do
+ view |> element("[data-testid=custom-field-delete]") |> render_click()
+ else
+ view |> element(row_selector, custom_field.name) |> render_click()
+ view |> element("[data-testid=custom-field-delete]") |> render_click()
+ end
end
describe "delete button and modal" do
@@ -71,8 +75,12 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
# Modal should be visible
assert has_element?(view, "#delete-custom-field-modal")
+ # Edit mode: section titles must not reappear when modal opens (regression)
+ refute has_element?(view, "h2", "Member fields")
+ refute has_element?(view, "h2", "Custom fields")
+
# Should show correct member count (1 member)
- assert render(view) =~ "1 member has a value assigned for this custom field"
+ assert render(view) =~ "1 member has a value assigned for this datafield"
# Should show the slug
assert render(view) =~ custom_field.slug
@@ -91,7 +99,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
open_delete_modal(view, custom_field)
# Should show plural form
- assert render(view) =~ "2 members have values assigned for this custom field"
+ assert render(view) =~ "2 members have values assigned for this datafield"
end
test "shows 0 members for custom field without values", %{conn: conn} do
@@ -101,7 +109,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
open_delete_modal(view, custom_field)
# Should show 0 members
- assert render(view) =~ "0 members have values assigned for this custom field"
+ assert render(view) =~ "0 members have values assigned for this datafield"
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
index d3c1612..4356279 100644
--- a/test/mv_web/live/member_field_live/index_component_test.exs
+++ b/test/mv_web/live/member_field_live/index_component_test.exs
@@ -83,6 +83,21 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do
end
end
+ describe "edit mode visibility" do
+ test "clicking member field row shows only form, no section titles", %{conn: conn} do
+ {:ok, view, _html} = live(conn, ~p"/admin/datafields")
+
+ # Row click is on the first td (no col_click); click that cell to open edit form
+ view
+ |> element("tr#member_field-first_name td:first-child")
+ |> render_click()
+
+ assert has_element?(view, "#member-field-form-first_name")
+ refute has_element?(view, "h2", "Custom fields")
+ refute has_element?(view, "h2", "Member fields")
+ end
+ end
+
describe "required fields" do
setup do
{:ok, settings} = Membership.get_settings()
diff --git a/test/mv_web/live/role_live_test.exs b/test/mv_web/live/role_live_test.exs
index 57ce814..2bbb443 100644
--- a/test/mv_web/live/role_live_test.exs
+++ b/test/mv_web/live/role_live_test.exs
@@ -386,11 +386,16 @@ defmodule MvWeb.RoleLiveTest do
{:ok, view, _html} = live(conn, "/admin/roles/#{role.id}")
- # Delete from Danger zone on show page
+ # Open delete modal from Danger zone
view
|> element("[data-testid=role-delete]")
|> render_click()
+ # Confirm deletion in modal
+ view
+ |> element("[data-testid=role-delete-confirm]")
+ |> render_click()
+
assert_redirect(view, "/admin/roles")
# Verify deletion by checking database
diff --git a/test/mv_web/live/statistics_live_test.exs b/test/mv_web/live/statistics_live_test.exs
index ed6128f..49c4167 100644
--- a/test/mv_web/live/statistics_live_test.exs
+++ b/test/mv_web/live/statistics_live_test.exs
@@ -29,9 +29,8 @@ defmodule MvWeb.StatisticsLiveTest do
test "page shows overview of all relevant years without year selector", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/statistics")
- # No year dropdown: single select for year should not be present as main control
- assert html =~ "Overview" or html =~ "overview"
- # table header or legend
+ # Page shows multi-year data (member numbers by year) and year column; no single-year selector as main control
+ assert html =~ "Member numbers by year"
assert html =~ "Year"
end
diff --git a/test/mv_web/member_live/index_groups_display_test.exs b/test/mv_web/member_live/index_groups_display_test.exs
index b28b978..263ac2a 100644
--- a/test/mv_web/member_live/index_groups_display_test.exs
+++ b/test/mv_web/member_live/index_groups_display_test.exs
@@ -95,6 +95,20 @@ defmodule MvWeb.MemberLive.IndexGroupsDisplayTest do
assert html =~ member3.first_name
end
+ test "empty group cell is visually empty with sr-only text (no dash)", %{
+ conn: conn,
+ member3: member3
+ } do
+ conn = conn_with_oidc_user(conn)
+ {:ok, _view, html} = live(conn, "/members")
+ assert html =~ member3.first_name
+ # Screen reader gets a meaningful label for the empty cell
+ assert html =~ "sr-only"
+ assert html =~ "No group assignment"
+ # No visible dash as placeholder (Design Guidelines §8.6)
+ refute html =~ ~r/]*class="[^"]*text-base-content\/50[^"]*"[^>]*>—<\/span>/
+ end
+
test "displays group name correctly in badge", %{conn: conn, group1: group1} do
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
diff --git a/test/mv_web/user_live/index_test.exs b/test/mv_web/user_live/index_test.exs
index f748000..c0be795 100644
--- a/test/mv_web/user_live/index_test.exs
+++ b/test/mv_web/user_live/index_test.exs
@@ -123,13 +123,17 @@ defmodule MvWeb.UserLive.IndexTest do
{:ok, index_view, _html} = live(conn, "/users")
assert render(index_view) =~ "delete-me@example.com"
- # Navigate to user show and trigger delete from Danger zone
+ # Navigate to user show, open delete modal, then confirm in modal (WCAG modal pattern)
{:ok, show_view, _html} = live(conn, "/users/#{user.id}")
show_view
|> element("[data-testid=user-delete]")
|> render_click()
+ show_view
+ |> element("#delete-user-modal button", "Delete")
+ |> render_click()
+
# Should redirect to index
assert_redirect(show_view, "/users")
@@ -206,7 +210,9 @@ defmodule MvWeb.UserLive.IndexTest do
end
describe "Password column display" do
- test "user without password shows em dash in Password column", %{conn: conn} do
+ test "user without password shows empty cell with sr-only text in Password column", %{
+ conn: conn
+ } do
# User created with hashed_password: nil (no password) - must not get default password
user_no_pw =
create_test_user(%{
@@ -219,9 +225,13 @@ defmodule MvWeb.UserLive.IndexTest do
assert html =~ "no-password@example.com"
- # Password column must show "—" (em dash) for user without password, not "Enabled"
+ # Password column: visually empty, screen-reader gets "Not set" (Design Guidelines §8.6)
row = view |> element("tr#row-#{user_no_pw.id}") |> render()
- assert row =~ "—", "Password column should show em dash for user without password"
+ assert row =~ "sr-only", "Password column should have sr-only text for accessibility"
+ assert row =~ "Not set", "Screen reader should get 'Not set' for empty password"
+
+ refute row =~ "—",
+ "Password column must not show dash (use empty cell + sr-only per CODE_GUIDELINES §8)"
refute row =~ "Enabled",
"Password column must not show Enabled when user has no password"