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 alias MvWeb.Helpers.MembershipFeeHelpers @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")}

{@member.first_name} {@member.last_name}

<.button variant="primary" navigate={~p"/members/#{@member}/edit?return_to=show"}> {gettext("Edit Member")}
<%!-- Tab Navigation --%>
<%= if @active_tab == :contact do %> <%!-- Contact Data Tab Content --%> <%!-- Personal Data and Custom Fields Row --%>
<%!-- Personal Data Section --%>
<.section_box title={gettext("Personal Data")}>
<%!-- Name Row --%>
<.data_field label={gettext("First Name")} value={@member.first_name} class="w-48" /> <.data_field label={gettext("Last Name")} value={@member.last_name} class="w-48" />
<%!-- Address --%>
<.data_field label={gettext("Address")} value={format_address(@member)} />
<%!-- Email --%>
<.data_field label={gettext("Email")}> {@member.email}
<%!-- Phone --%>
<.data_field label={gettext("Phone")} value={@member.phone_number} />
<%!-- Membership Dates Row --%>
<.data_field label={gettext("Join Date")} value={format_date(@member.join_date)} class="w-28" /> <.data_field label={gettext("Exit Date")} value={format_date(@member.exit_date)} class="w-28" />
<%!-- Linked User --%>
<.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?(@member.custom_field_values) do %>
<.section_box title={gettext("Custom Fields")}>
<%= for cfv <- sort_custom_field_values(@member.custom_field_values) do %> <% custom_field = cfv.custom_field %> <% value_type = custom_field && custom_field.value_type %> <.data_field label={custom_field && custom_field.name}> {format_custom_field_value(cfv.value, value_type)} <% end %>
<% end %>
<%!-- Payment Data Section --%>
<.section_box title={gettext("Payment Data")}> <%= if @member.membership_fee_type do %>
<.data_field label={gettext("Type")} value={@member.membership_fee_type.name} class="w-32" /> <.data_field label={gettext("Membership Fee")} value={MembershipFeeHelpers.format_currency(@member.membership_fee_type.amount)} class="w-24" /> <.data_field label={gettext("Payment Cycle")} value={MembershipFeeHelpers.format_interval(@member.membership_fee_type.interval)} class="w-28" /> <.data_field label={gettext("Last Cycle")} class="w-28 whitespace-nowrap"> <%= if @member.last_cycle_status do %> <% status = @member.last_cycle_status %> {format_status_label(status)} <% else %> {gettext("No cycles")} <% end %> <.data_field label={gettext("Current Cycle")} class="w-32 whitespace-nowrap"> <%= if @member.current_cycle_status do %> <% status = @member.current_cycle_status %> {format_status_label(status)} <% else %> {gettext("No cycles")} <% end %>
<% else %>
{gettext("No membership fee type assigned")}
<% end %>
<% end %> <%= if @active_tab == :membership_fees do %> <%!-- Membership Fees Tab Content --%> <.live_component module={MvWeb.MemberLive.Show.MembershipFeesComponent} id={"membership-fees-#{@member.id}"} member={@member} /> <% end %>
""" end @impl true def mount(_params, _session, socket) do {:ok, assign(socket, :active_tab, :contact)} end @impl true def handle_params(%{"id" => id}, _, socket) do query = Mv.Membership.Member |> filter(id == ^id) |> load([ :user, :membership_fee_type, custom_field_values: [:custom_field], membership_fee_cycles: [:membership_fee_type] ]) member = Ash.read_one!(query) # Calculate last and current cycle status from loaded cycles last_cycle_status = get_last_cycle_status(member) current_cycle_status = get_current_cycle_status(member) member = member |> Map.put(:last_cycle_status, last_cycle_status) |> Map.put(:current_cycle_status, current_cycle_status) {:noreply, socket |> assign(:page_title, page_title(socket.assigns.live_action)) |> assign(:member, member)} end @impl true def handle_event("switch_tab", %{"tab" => "contact"}, socket) do {:noreply, assign(socket, :active_tab, :contact)} end def handle_event("switch_tab", %{"tab" => "membership_fees"}, socket) do {:noreply, assign(socket, :active_tab, :membership_fees)} 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_status_label(:paid), do: gettext("Paid") defp format_status_label(:unpaid), do: gettext("Unpaid") defp format_status_label(:suspended), do: gettext("Suspended") defp format_status_label(nil), do: gettext("No status") defp get_last_cycle_status(member) do case MembershipFeeHelpers.get_last_completed_cycle(member) do nil -> nil cycle -> cycle.status end end defp get_current_cycle_status(member) do case MembershipFeeHelpers.get_current_cycle(member) do nil -> nil cycle -> cycle.status end end 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) # Sorts custom field values by custom field name defp sort_custom_field_values(custom_field_values) do Enum.sort_by(custom_field_values, fn cfv -> (cfv.custom_field && cfv.custom_field.name) || "" end) end # Formats custom field value based on type 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 assigns = %{email: value} ~H""" {@email} """ 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