defmodule MvWeb.MemberLive.Show do
@moduledoc """
LiveView for displaying a single member's details.
## Features
- Display all member information in grouped sections
- Tab navigation for future features (Payments)
- Show custom field values with type-based formatting
- Navigate to edit form
- Return to member list
## Sections
- Personal Data: Name, address, contact information, membership dates, notes
- Custom Fields: Dynamic fields in uniform grid layout (sorted by name)
- Payment Data: Mockup section with placeholder data
## Navigation
- Back to member list
- Edit member (with return_to parameter for back navigation)
"""
use MvWeb, :live_view
import Ash.Query
@impl true
def render(assigns) do
~H"""
<%!-- Header with Back button, Name, and Edit button --%>
<.button navigate={~p"/members"} aria-label={gettext("Back to members list")}>
<.icon name="hero-arrow-left" class="size-4" />
{gettext("Back")}
<.data_field label={gettext("Linked User")}>
<%= if @member.user do %>
<.link
navigate={~p"/users/#{@member.user}"}
class="text-blue-700 hover:text-blue-800 underline inline-flex items-center gap-1"
>
<.icon name="hero-user" class="size-4" />
{@member.user.email}
<% else %>
{gettext("No user linked")}
<% end %>
<%!-- Notes --%>
<%= if @member.notes && String.trim(@member.notes) != "" do %>
<.data_field label={gettext("Notes")}>
{@member.notes}
<% end %>
<%!-- Custom Fields Section --%>
<%= if Enum.any?(@custom_fields_with_values) do %>
<.section_box title={gettext("Custom Fields")}>
<%= for {custom_field, cfv} <- @custom_fields_with_values do %>
<.data_field label={custom_field.name}>
<%= if cfv && cfv.value do %>
<%= if custom_field.value_type == :email && cfv.value.value && String.trim(cfv.value.value) != "" do %>
{cfv.value.value}
<% else %>
{format_custom_field_value(cfv.value, custom_field.value_type)}
<% end %>
<% else %>
{format_custom_field_value(nil, custom_field.value_type)}
<% end %>
<% end %>
<% end %>
<%!-- Payment Data Section (Mockup) --%>
<.section_box title={gettext("Payment Data")}>
<.icon name="hero-information-circle" class="size-5" />
{gettext("This data is for demonstration purposes only (mockup).")}
<.data_field label={gettext("Contribution")} value="72 €" class="w-24" />
<.data_field label={gettext("Payment Cycle")} value={gettext("monthly")} class="w-28" />
<.data_field label={gettext("Paid")} class="w-24">
<%= if @member.paid do %>
{gettext("Paid")}
<% else %>
{gettext("Pending")}
<% end %>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :custom_fields_with_values, [])}
end
@impl true
def handle_params(%{"id" => id}, _, socket) do
query =
Mv.Membership.Member
|> filter(id == ^id)
|> load([:user, custom_field_values: [:custom_field]])
member = Ash.read_one!(query)
# Load all custom fields to display all of them, even if they have no values
{:ok, custom_fields} = Mv.Membership.list_custom_fields()
# Create a map of custom_field_id -> custom_field_value for quick lookup
custom_field_values_map =
member.custom_field_values
|> Enum.map(fn cfv -> {cfv.custom_field_id, cfv} end)
|> Map.new()
# Match all custom fields with their values (if they exist)
custom_fields_with_values =
Enum.map(custom_fields, fn cf ->
cfv = Map.get(custom_field_values_map, cf.id)
{cf, cfv}
end)
|> Enum.sort_by(fn {cf, _cfv} -> cf.name end)
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:member, member)
|> assign(:custom_fields_with_values, custom_fields_with_values)}
end
defp page_title(:show), do: gettext("Show Member")
defp page_title(:edit), do: gettext("Edit Member")
# -----------------------------------------------------------------
# Helper Components
# -----------------------------------------------------------------
# Renders a section box with border and title.
attr :title, :string, required: true
slot :inner_block, required: true
defp section_box(assigns) do
~H"""
{@title}
{render_slot(@inner_block)}
"""
end
# Renders a labeled data field.
attr :label, :string, required: true
attr :value, :string, default: nil
attr :class, :string, default: ""
slot :inner_block
defp data_field(assigns) do
~H"""
{@label}
<%= if @inner_block != [] do %>
{render_slot(@inner_block)}
<% else %>
{display_value(@value)}
<% end %>
"""
end
# -----------------------------------------------------------------
# Helper Functions
# -----------------------------------------------------------------
defp display_value(nil), do: ""
defp display_value(""), do: ""
defp display_value(value), do: value
defp format_address(member) do
street_part =
[member.street, member.house_number]
|> Enum.filter(&(&1 && &1 != ""))
|> Enum.join(" ")
city_part =
[member.postal_code, member.city]
|> Enum.filter(&(&1 && &1 != ""))
|> Enum.join(" ")
[street_part, city_part]
|> Enum.filter(&(&1 != ""))
|> Enum.join(", ")
|> case do
"" -> nil
address -> address
end
end
defp format_date(nil), do: nil
defp format_date(%Date{} = date) do
Calendar.strftime(date, "%d.%m.%Y")
end
defp format_date(date), do: to_string(date)
# Formats custom field value based on type
# Returns empty string for nil/empty values (consistent with member fields behavior)
defp format_custom_field_value(%Ash.Union{value: value, type: type}, _expected_type) do
format_custom_field_value(value, type)
end
defp format_custom_field_value(nil, _type), do: ""
defp format_custom_field_value(value, :boolean) when is_boolean(value) do
if value, do: gettext("Yes"), else: gettext("No")
end
defp format_custom_field_value(%Date{} = date, :date) do
Calendar.strftime(date, "%d.%m.%Y")
end
defp format_custom_field_value(value, :email) when is_binary(value) do
if String.trim(value) == "", do: "", else: value
end
defp format_custom_field_value(value, :integer) when is_integer(value) do
Integer.to_string(value)
end
defp format_custom_field_value(value, _type) when is_binary(value) do
if String.trim(value) == "", do: "", else: value
end
defp format_custom_field_value(value, _type), do: to_string(value)
end