diff --git a/lib/mv_web/live/contribution_period_live/show.ex b/lib/mv_web/live/contribution_period_live/show.ex
index 83d9207..f297bf2 100644
--- a/lib/mv_web/live/contribution_period_live/show.ex
+++ b/lib/mv_web/live/contribution_period_live/show.ex
@@ -36,7 +36,7 @@ defmodule MvWeb.ContributionPeriodLive.Show do
<.mockup_warning />
<.header>
- {gettext("Contributions for %{name}", name: "#{@member.first_name} #{@member.last_name}")}
+ {gettext("Contributions for %{name}", name: MvWeb.MemberLive.Index.display_name(@member))}
<:subtitle>
{gettext("Contribution type")}:
{@member.contribution_type}
diff --git a/lib/mv_web/live/custom_field_value_live/form.ex b/lib/mv_web/live/custom_field_value_live/form.ex
index 9663927..4ed1a23 100644
--- a/lib/mv_web/live/custom_field_value_live/form.ex
+++ b/lib/mv_web/live/custom_field_value_live/form.ex
@@ -289,6 +289,6 @@ defmodule MvWeb.CustomFieldValueLive.Form do
end
defp member_options(members) do
- Enum.map(members, &{"#{&1.first_name} #{&1.last_name}", &1.id})
+ Enum.map(members, &{MvWeb.MemberLive.Index.display_name(&1), &1.id})
end
end
diff --git a/lib/mv_web/live/member_live/form.ex b/lib/mv_web/live/member_live/form.ex
index d5b2c3a..16ad195 100644
--- a/lib/mv_web/live/member_live/form.ex
+++ b/lib/mv_web/live/member_live/form.ex
@@ -43,7 +43,7 @@ defmodule MvWeb.MemberLive.Form do
<%= if @member do %>
- {@member.first_name} {@member.last_name}
+ {MvWeb.MemberLive.Index.display_name(@member)}
<% else %>
{gettext("New Member")}
<% end %>
diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex
index fff5517..57aa630 100644
--- a/lib/mv_web/live/member_live/index.ex
+++ b/lib/mv_web/live/member_live/index.ex
@@ -1165,6 +1165,62 @@ defmodule MvWeb.MemberLive.Index do
end
end
+ @doc """
+ Returns a display name for a member.
+
+ Combines first_name and last_name if available, otherwise falls back to email.
+ This ensures that members without names still have a meaningful display name.
+
+ ## Examples
+
+ iex> member = %Member{first_name: "John", last_name: "Doe", email: "john@example.com"}
+ iex> display_name(member)
+ "John Doe"
+
+ iex> member = %Member{first_name: nil, last_name: nil, email: "john@example.com"}
+ iex> display_name(member)
+ "john@example.com"
+
+ iex> member = %Member{first_name: "John", last_name: nil, email: "john@example.com"}
+ iex> display_name(member)
+ "John"
+ """
+ def display_name(member) do
+ name_parts =
+ [member.first_name, member.last_name]
+ |> Enum.reject(&blank?/1)
+ |> Enum.map(&String.trim/1)
+ |> Enum.join(" ")
+
+ if name_parts == "" do
+ member.email
+ else
+ name_parts
+ end
+ end
+
+ @doc """
+ Checks if a value is blank (nil, empty string, or only whitespace).
+
+ ## Examples
+
+ iex> blank?(nil)
+ true
+
+ iex> blank?("")
+ true
+
+ iex> blank?(" ")
+ true
+
+ iex> blank?("John")
+ false
+ """
+ def blank?(nil), do: true
+ def blank?(""), do: true
+ def blank?(value) when is_binary(value), do: String.trim(value) == ""
+ def blank?(_), do: false
+
# Public helper function to format dates for use in templates
def format_date(date), do: DateFormatter.format_date(date)
diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex
index 1557ed9..430a601 100644
--- a/lib/mv_web/live/member_live/index.html.heex
+++ b/lib/mv_web/live/member_live/index.html.heex
@@ -129,7 +129,12 @@
"""
}
>
- {member.first_name}
+ {if MvWeb.MemberLive.Index.blank?(member.first_name) &&
+ MvWeb.MemberLive.Index.blank?(member.last_name) do
+ MvWeb.MemberLive.Index.display_name(member)
+ else
+ member.first_name
+ end}
<:col
:let={member}
diff --git a/lib/mv_web/live/member_live/show.ex b/lib/mv_web/live/member_live/show.ex
index ef2244e..e9236fd 100644
--- a/lib/mv_web/live/member_live/show.ex
+++ b/lib/mv_web/live/member_live/show.ex
@@ -35,7 +35,7 @@ defmodule MvWeb.MemberLive.Show do
- {@member.first_name} {@member.last_name}
+ {MvWeb.MemberLive.Index.display_name(@member)}
<.button variant="primary" navigate={~p"/members/#{@member}/edit?return_to=show"}>
diff --git a/lib/mv_web/live/user_live/form.ex b/lib/mv_web/live/user_live/form.ex
index 0639e75..85e5bbb 100644
--- a/lib/mv_web/live/user_live/form.ex
+++ b/lib/mv_web/live/user_live/form.ex
@@ -131,7 +131,7 @@ defmodule MvWeb.UserLive.Form do
- {@user.member.first_name} {@user.member.last_name}
+ {MvWeb.MemberLive.Index.display_name(@user.member)}
{@user.member.email}
@@ -210,7 +210,7 @@ defmodule MvWeb.UserLive.Form do
)
]}
>
-
{member.first_name} {member.last_name}
+
{MvWeb.MemberLive.Index.display_name(member)}
{member.email}
<% end %>
@@ -438,7 +438,7 @@ defmodule MvWeb.UserLive.Form do
member_name =
if selected_member,
- do: "#{selected_member.first_name} #{selected_member.last_name}",
+ do: MvWeb.MemberLive.Index.display_name(selected_member),
else: ""
# Store the selected member ID and name in socket state and clear unlink flag
diff --git a/lib/mv_web/live/user_live/index.html.heex b/lib/mv_web/live/user_live/index.html.heex
index 9a98159..c496ea8 100644
--- a/lib/mv_web/live/user_live/index.html.heex
+++ b/lib/mv_web/live/user_live/index.html.heex
@@ -51,7 +51,7 @@
<:col :let={user} label={gettext("Linked Member")}>
<%= if user.member do %>
- {user.member.first_name} {user.member.last_name}
+ {MvWeb.MemberLive.Index.display_name(user.member)}
<% else %>
{gettext("No member linked")}
<% end %>
diff --git a/lib/mv_web/live/user_live/show.ex b/lib/mv_web/live/user_live/show.ex
index 777def1..f05a763 100644
--- a/lib/mv_web/live/user_live/show.ex
+++ b/lib/mv_web/live/user_live/show.ex
@@ -57,7 +57,7 @@ defmodule MvWeb.UserLive.Show do
class="text-blue-600 underline hover:text-blue-800"
>
<.icon name="hero-users" class="inline w-4 h-4 mr-1" />
- {@user.member.first_name} {@user.member.last_name}
+ {MvWeb.MemberLive.Index.display_name(@user.member)}
<% else %>
{gettext("No member linked")}
diff --git a/test/mv_web/member_live/index_display_name_test.exs b/test/mv_web/member_live/index_display_name_test.exs
new file mode 100644
index 0000000..86758c9
--- /dev/null
+++ b/test/mv_web/member_live/index_display_name_test.exs
@@ -0,0 +1,141 @@
+defmodule MvWeb.MemberLive.Index.DisplayNameTest do
+ @moduledoc """
+ Tests for the display_name/1 helper function in MemberLive.Index.
+ """
+ use Mv.DataCase, async: true
+
+ alias Mv.Membership.Member
+ alias MvWeb.MemberLive.Index
+
+ describe "display_name/1" do
+ test "returns full name when both first_name and last_name are present" do
+ member = %Member{
+ first_name: "John",
+ last_name: "Doe",
+ email: "john@example.com"
+ }
+
+ assert Index.display_name(member) == "John Doe"
+ end
+
+ test "returns email when both first_name and last_name are nil" do
+ member = %Member{
+ first_name: nil,
+ last_name: nil,
+ email: "john@example.com"
+ }
+
+ assert Index.display_name(member) == "john@example.com"
+ end
+
+ test "returns first_name only when last_name is nil" do
+ member = %Member{
+ first_name: "John",
+ last_name: nil,
+ email: "john@example.com"
+ }
+
+ assert Index.display_name(member) == "John"
+ end
+
+ test "returns last_name only when first_name is nil" do
+ member = %Member{
+ first_name: nil,
+ last_name: "Doe",
+ email: "john@example.com"
+ }
+
+ assert Index.display_name(member) == "Doe"
+ end
+
+ test "returns email when first_name and last_name are empty strings" do
+ member = %Member{
+ first_name: "",
+ last_name: "",
+ email: "john@example.com"
+ }
+
+ assert Index.display_name(member) == "john@example.com"
+ end
+
+ test "returns email when first_name and last_name are whitespace only" do
+ member = %Member{
+ first_name: " ",
+ last_name: " \t ",
+ email: "john@example.com"
+ }
+
+ assert Index.display_name(member) == "john@example.com"
+ end
+
+ test "trims whitespace from name parts" do
+ member = %Member{
+ first_name: " John ",
+ last_name: " Doe ",
+ email: "john@example.com"
+ }
+
+ assert Index.display_name(member) == "John Doe"
+ end
+
+ test "handles one empty string and one nil" do
+ member = %Member{
+ first_name: "",
+ last_name: nil,
+ email: "john@example.com"
+ }
+
+ assert Index.display_name(member) == "john@example.com"
+ end
+
+ test "handles one nil and one empty string" do
+ member = %Member{
+ first_name: nil,
+ last_name: "",
+ email: "john@example.com"
+ }
+
+ assert Index.display_name(member) == "john@example.com"
+ end
+
+ test "handles one whitespace and one nil" do
+ member = %Member{
+ first_name: " ",
+ last_name: nil,
+ email: "john@example.com"
+ }
+
+ assert Index.display_name(member) == "john@example.com"
+ end
+
+ test "handles one valid name and one whitespace" do
+ member = %Member{
+ first_name: "John",
+ last_name: " ",
+ email: "john@example.com"
+ }
+
+ assert Index.display_name(member) == "John"
+ end
+
+ test "handles member with only first_name containing whitespace" do
+ member = %Member{
+ first_name: " John ",
+ last_name: nil,
+ email: "john@example.com"
+ }
+
+ assert Index.display_name(member) == "John"
+ end
+
+ test "handles member with only last_name containing whitespace" do
+ member = %Member{
+ first_name: nil,
+ last_name: " Doe ",
+ email: "john@example.com"
+ }
+
+ assert Index.display_name(member) == "Doe"
+ end
+ end
+end