refactor: use core components

This commit is contained in:
carla 2026-02-25 09:12:33 +01:00
parent f0be98316c
commit b7c93f19cb
26 changed files with 1080 additions and 954 deletions

View file

@ -38,227 +38,226 @@ defmodule MvWeb.MemberLive.Form do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.form for={@form} id="member-form" phx-change="validate" phx-submit="save">
<%!-- Header with Back button, Name display, and Save button --%>
<div class="flex items-center justify-between gap-4 pb-4">
<.button navigate={return_path(@return_to, @member)} type="button">
<.icon name="hero-arrow-left" class="size-4" />
{gettext("Back")}
</.button>
<.header>
<%= if @member do %>
{MvWeb.Helpers.MemberHelpers.display_name(@member)}
<% else %>
{gettext("New Member")}
<% end %>
<:actions>
<.button navigate={return_path(@return_to, @member)} variant="neutral">
<.icon name="hero-arrow-left" class="size-4" />
{gettext("Back")}
</.button>
<.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit">
{gettext("Save")}
</.button>
</:actions>
</.header>
<h1 class="text-2xl font-bold text-center flex-1">
<%= if @member do %>
{MvWeb.Helpers.MemberHelpers.display_name(@member)}
<% else %>
{gettext("New Member")}
<div class="mt-6 space-y-6">
<%!-- Tab Navigation --%>
<div role="tablist" class="tabs tabs-bordered">
<button type="button" role="tab" class="tab tab-active" aria-selected="true">
<.icon name="hero-identification" class="size-4 mr-2" />
{gettext("Contact Data")}
</button>
<button
type="button"
role="tab"
class="tab"
disabled
aria-disabled="true"
title={gettext("Coming soon")}
>
<.icon name="hero-credit-card" class="size-4 mr-2" />
{gettext("Payments")}
</button>
</div>
<%!-- Personal Data and Custom Fields Row --%>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<%!-- Personal Data Section --%>
<div>
<.form_section title={gettext("Personal Data")}>
<div class="space-y-4">
<%!-- Name Row --%>
<div class="flex gap-4">
<div class="w-48">
<.input
field={@form[:first_name]}
label={gettext("First Name")}
required={@member_field_required_map[:first_name]}
/>
</div>
<div class="w-48">
<.input
field={@form[:last_name]}
label={gettext("Last Name")}
required={@member_field_required_map[:last_name]}
/>
</div>
</div>
<%!-- Address Row --%>
<div class="flex gap-4">
<div class="flex-1">
<.input
field={@form[:street]}
label={gettext("Street")}
required={@member_field_required_map[:street]}
/>
</div>
<div class="w-16">
<.input
field={@form[:house_number]}
label={gettext("Nr.")}
required={@member_field_required_map[:house_number]}
/>
</div>
<div class="w-24">
<.input
field={@form[:postal_code]}
label={gettext("Postal Code")}
required={@member_field_required_map[:postal_code]}
/>
</div>
<div class="w-32">
<.input
field={@form[:city]}
label={gettext("City")}
required={@member_field_required_map[:city]}
/>
</div>
</div>
<%!-- Email (always required) --%>
<div>
<.input field={@form[:email]} label={gettext("Email")} required type="email" />
</div>
<%!-- Membership Dates Row --%>
<div class="flex gap-4">
<div class="w-36">
<.input
field={@form[:join_date]}
label={gettext("Join Date")}
type="date"
required={@member_field_required_map[:join_date]}
/>
</div>
<div class="w-36">
<.input
field={@form[:exit_date]}
label={gettext("Exit Date")}
type="date"
required={@member_field_required_map[:exit_date]}
/>
</div>
</div>
<%!-- Notes --%>
<div>
<.input
field={@form[:notes]}
label={gettext("Notes")}
type="textarea"
required={@member_field_required_map[:notes]}
/>
</div>
</div>
</.form_section>
</div>
<%!-- Custom Fields Section --%>
<%= if Enum.any?(@custom_fields) do %>
<div>
<.form_section title={gettext("Custom Fields")}>
<div class="grid grid-cols-2 gap-4">
<%!-- Render in sorted order by finding the form for each sorted custom field --%>
<%= for cf <- @sorted_custom_fields do %>
<.inputs_for :let={f_cfv} field={@form[:custom_field_values]}>
<%= if f_cfv[:custom_field_id].value == cf.id do %>
<div class={if cf.value_type == :boolean, do: "flex items-end", else: ""}>
<.inputs_for :let={value_form} field={f_cfv[:value]}>
<.input
field={value_form[:value]}
label={cf.name}
type={custom_field_input_type(cf.value_type)}
required={cf.required}
/>
</.inputs_for>
<input
type="hidden"
name={f_cfv[:custom_field_id].name}
value={f_cfv[:custom_field_id].value}
/>
</div>
<% end %>
</.inputs_for>
<% end %>
</div>
</.form_section>
</div>
<% end %>
</h1>
</div>
<.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit">
{gettext("Save")}
</.button>
</div>
<%!-- Tab Navigation --%>
<div role="tablist" class="tabs tabs-bordered mb-6">
<button type="button" role="tab" class="tab tab-active" aria-selected="true">
<.icon name="hero-identification" class="size-4 mr-2" />
{gettext("Contact Data")}
</button>
<button
type="button"
role="tab"
class="tab"
disabled
aria-disabled="true"
title={gettext("Coming soon")}
>
<.icon name="hero-credit-card" class="size-4 mr-2" />
{gettext("Payments")}
</button>
</div>
<%!-- Personal Data and Custom Fields Row --%>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<%!-- Personal Data Section --%>
<div>
<.form_section title={gettext("Personal Data")}>
<%!-- Membership Fee Section --%>
<div class="max-w-xl">
<.form_section title={gettext("Membership Fee")}>
<div class="space-y-4">
<%!-- Name Row --%>
<div class="flex gap-4">
<div class="w-48">
<.input
field={@form[:first_name]}
label={gettext("First Name")}
required={@member_field_required_map[:first_name]}
/>
</div>
<div class="w-48">
<.input
field={@form[:last_name]}
label={gettext("Last Name")}
required={@member_field_required_map[:last_name]}
/>
</div>
</div>
<%!-- Address Row --%>
<div class="flex gap-4">
<div class="flex-1">
<.input
field={@form[:street]}
label={gettext("Street")}
required={@member_field_required_map[:street]}
/>
</div>
<div class="w-16">
<.input
field={@form[:house_number]}
label={gettext("Nr.")}
required={@member_field_required_map[:house_number]}
/>
</div>
<div class="w-24">
<.input
field={@form[:postal_code]}
label={gettext("Postal Code")}
required={@member_field_required_map[:postal_code]}
/>
</div>
<div class="w-32">
<.input
field={@form[:city]}
label={gettext("City")}
required={@member_field_required_map[:city]}
/>
</div>
</div>
<%!-- Email (always required) --%>
<div>
<.input field={@form[:email]} label={gettext("Email")} required type="email" />
</div>
<%!-- Membership Dates Row --%>
<div class="flex gap-4">
<div class="w-36">
<.input
field={@form[:join_date]}
label={gettext("Join Date")}
type="date"
required={@member_field_required_map[:join_date]}
/>
</div>
<div class="w-36">
<.input
field={@form[:exit_date]}
label={gettext("Exit Date")}
type="date"
required={@member_field_required_map[:exit_date]}
/>
</div>
</div>
<%!-- Notes --%>
<div>
<.input
field={@form[:notes]}
label={gettext("Notes")}
type="textarea"
required={@member_field_required_map[:notes]}
/>
<label class="label">
<span class="label-text font-semibold">{gettext("Membership Fee Type")}</span>
</label>
<select
class="select select-bordered w-full"
name={@form[:membership_fee_type_id].name}
phx-change="validate"
value={@form[:membership_fee_type_id].value || ""}
>
<%!-- No "None" option: a membership fee type is required (validated in Member resource). --%>
<option value="">{gettext("Select a membership fee type")}</option>
<%= for fee_type <- @available_fee_types do %>
<option
value={fee_type.id}
selected={fee_type.id == @form[:membership_fee_type_id].value}
>
{fee_type.name} ({MembershipFeeHelpers.format_currency(fee_type.amount)}, {MembershipFeeHelpers.format_interval(
fee_type.interval
)})
</option>
<% end %>
</select>
<%= for error <- List.wrap(@form.errors[:membership_fee_type_id] || []) do %>
<% {msg, _opts} = if is_tuple(error), do: error, else: {error, []} %>
<p class="text-error text-sm mt-1">{msg}</p>
<% end %>
<%= if @interval_warning do %>
<div class="alert alert-warning mt-2">
<.icon name="hero-exclamation-triangle" class="size-5" />
<span>{@interval_warning}</span>
</div>
<% end %>
<p class="text-sm text-base-content/60 mt-2">
{gettext(
"Select a membership fee type for this member. Members can only switch between types with the same interval."
)}
</p>
</div>
</div>
</.form_section>
</div>
<%!-- Custom Fields Section --%>
<%= if Enum.any?(@custom_fields) do %>
<div>
<.form_section title={gettext("Custom Fields")}>
<div class="grid grid-cols-2 gap-4">
<%!-- Render in sorted order by finding the form for each sorted custom field --%>
<%= for cf <- @sorted_custom_fields do %>
<.inputs_for :let={f_cfv} field={@form[:custom_field_values]}>
<%= if f_cfv[:custom_field_id].value == cf.id do %>
<div class={if cf.value_type == :boolean, do: "flex items-end", else: ""}>
<.inputs_for :let={value_form} field={f_cfv[:value]}>
<.input
field={value_form[:value]}
label={cf.name}
type={custom_field_input_type(cf.value_type)}
required={cf.required}
/>
</.inputs_for>
<input
type="hidden"
name={f_cfv[:custom_field_id].name}
value={f_cfv[:custom_field_id].value}
/>
</div>
<% end %>
</.inputs_for>
<% end %>
</div>
</.form_section>
</div>
<% end %>
</div>
<%!-- Membership Fee Section --%>
<div class="max-w-xl">
<.form_section title={gettext("Membership Fee")}>
<div class="space-y-4">
<div>
<label class="label">
<span class="label-text font-semibold">{gettext("Membership Fee Type")}</span>
</label>
<select
class="select select-bordered w-full"
name={@form[:membership_fee_type_id].name}
phx-change="validate"
value={@form[:membership_fee_type_id].value || ""}
>
<%!-- No "None" option: a membership fee type is required (validated in Member resource). --%>
<option value="">{gettext("Select a membership fee type")}</option>
<%= for fee_type <- @available_fee_types do %>
<option
value={fee_type.id}
selected={fee_type.id == @form[:membership_fee_type_id].value}
>
{fee_type.name} ({MembershipFeeHelpers.format_currency(fee_type.amount)}, {MembershipFeeHelpers.format_interval(
fee_type.interval
)})
</option>
<% end %>
</select>
<%= for error <- List.wrap(@form.errors[:membership_fee_type_id] || []) do %>
<% {msg, _opts} = if is_tuple(error), do: error, else: {error, []} %>
<p class="text-error text-sm mt-1">{msg}</p>
<% end %>
<%= if @interval_warning do %>
<div class="alert alert-warning mt-2">
<.icon name="hero-exclamation-triangle" class="size-5" />
<span>{@interval_warning}</span>
</div>
<% end %>
<p class="text-sm text-base-content/60 mt-2">
{gettext(
"Select a membership fee type for this member. Members can only switch between types with the same interval."
)}
</p>
</div>
</div>
</.form_section>
</div>
<%!-- Bottom Action Buttons --%>
<div class="flex justify-end gap-4 mt-6">
<.button navigate={return_path(@return_to, @member)} type="button">
{gettext("Cancel")}
</.button>
<.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit">
{gettext("Save Member")}
</.button>
<%!-- Bottom Action Buttons --%>
<div class="flex justify-end gap-4 mt-6">
<.button navigate={return_path(@return_to, @member)} variant="neutral" type="button">
{gettext("Cancel")}
</.button>
<.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit">
{gettext("Save Member")}
</.button>
</div>
</div>
</.form>
</Layouts.app>

View file

@ -9,7 +9,7 @@
selected_count={@selected_count}
/>
<.button
class="secondary"
variant="secondary"
id="copy-emails-btn"
phx-hook="CopyToClipboard"
phx-click="copy_emails"
@ -20,7 +20,7 @@
{gettext("Copy email addresses")} ({@selected_count})
</.button>
<.button
class="secondary"
variant="secondary"
id="open-email-btn"
href={"mailto:?bcc=" <> @mailto_bcc}
disabled={not @any_selected?}
@ -54,13 +54,11 @@
boolean_filters={@boolean_custom_field_filters}
member_count={length(@members)}
/>
<button
<.button
type="button"
variant="secondary"
class={["gap-2", @show_current_cycle && "btn-active"]}
phx-click="toggle_cycle_view"
class={[
"btn gap-2",
@show_current_cycle && "btn-active"
]}
aria-label={
if(@show_current_cycle,
do: gettext("Current Cycle Payment Status"),
@ -81,7 +79,7 @@
else: gettext("Last Cycle Payment Status")
)}
</span>
</button>
</.button>
<.live_component
module={MvWeb.Components.FieldVisibilityDropdownComponent}
id="field-visibility-dropdown"
@ -386,7 +384,7 @@
<%= if can?(@current_user, :update, member) do %>
<.link navigate={~p"/members/#{member}/edit"} data-testid="member-edit">
{gettext("Edit")}
{gettext("Edit member")}
</.link>
<% end %>
</:action>

View file

@ -30,235 +30,243 @@ defmodule MvWeb.MemberLive.Show do
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<%!-- Header with Back button, Name, and Edit button --%>
<div class="flex items-center justify-between gap-4 pb-4">
<.button navigate={~p"/members"} aria-label={gettext("Back to members list")}>
<.icon name="hero-arrow-left" class="size-4" />
{gettext("Back")}
</.button>
<h1 class="text-2xl font-bold text-center flex-1">
{MvWeb.Helpers.MemberHelpers.display_name(@member)}
</h1>
<%= if can?(@current_user, :update, @member) do %>
<.header>
{MvWeb.Helpers.MemberHelpers.display_name(@member)}
<:actions>
<.button
variant="primary"
navigate={~p"/members/#{@member}/edit?return_to=show"}
data-testid="member-edit"
navigate={~p"/members"}
variant="neutral"
aria-label={gettext("Back to members list")}
>
{gettext("Edit Member")}
<.icon name="hero-arrow-left" class="size-4" />
{gettext("Back")}
</.button>
<% end %>
</div>
<%= if can?(@current_user, :update, @member) do %>
<.button
variant="primary"
navigate={~p"/members/#{@member}/edit?return_to=show"}
data-testid="member-edit"
>
{gettext("Edit member")}
</.button>
<% end %>
</:actions>
</.header>
<%!-- Tab Navigation --%>
<div role="tablist" class="tabs tabs-bordered mb-6">
<button
role="tab"
class={[
"tab",
if(@active_tab == :contact, do: "tab-active", else: "!text-gray-800")
]}
aria-selected={@active_tab == :contact}
phx-click="switch_tab"
phx-value-tab="contact"
>
<.icon name="hero-identification" class="size-4 mr-2" />
{gettext("Contact Data")}
</button>
<button
role="tab"
class={[
"tab",
if(@active_tab == :membership_fees, do: "tab-active", else: "!text-gray-800")
]}
aria-selected={@active_tab == :membership_fees}
phx-click="switch_tab"
phx-value-tab="membership_fees"
>
<.icon name="hero-credit-card" class="size-4 mr-2" />
{gettext("Membership Fees")}
</button>
</div>
<div class="mt-6 space-y-6">
<%!-- Tab Navigation --%>
<div role="tablist" class="tabs tabs-bordered">
<button
role="tab"
class={[
"tab",
if(@active_tab == :contact, do: "tab-active", else: "!text-gray-800")
]}
aria-selected={@active_tab == :contact}
phx-click="switch_tab"
phx-value-tab="contact"
>
<.icon name="hero-identification" class="size-4 mr-2" />
{gettext("Contact Data")}
</button>
<button
role="tab"
class={[
"tab",
if(@active_tab == :membership_fees, do: "tab-active", else: "!text-gray-800")
]}
aria-selected={@active_tab == :membership_fees}
phx-click="switch_tab"
phx-value-tab="membership_fees"
>
<.icon name="hero-credit-card" class="size-4 mr-2" />
{gettext("Membership Fees")}
</button>
</div>
<%= if @active_tab == :contact do %>
<%!-- Contact Data Tab Content --%>
<%!-- Personal Data and Custom Fields Row --%>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<%!-- Personal Data Section --%>
<div>
<.section_box title={gettext("Personal Data")}>
<div class="space-y-4">
<%!-- Name Row --%>
<div class="flex gap-6">
<.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" />
</div>
<%= if @active_tab == :contact do %>
<%!-- Contact Data Tab Content --%>
<%!-- Personal Data and Custom Fields Row --%>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<%!-- Personal Data Section --%>
<div>
<.section_box title={gettext("Personal Data")}>
<div class="space-y-4">
<%!-- Name Row --%>
<div class="flex gap-6">
<.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" />
</div>
<%!-- Address --%>
<div>
<.data_field label={gettext("Address")} value={format_address(@member)} />
</div>
<%!-- Address --%>
<div>
<.data_field label={gettext("Address")} value={format_address(@member)} />
</div>
<%!-- Email --%>
<div>
<.data_field label={gettext("Email")}>
<a
href={"mailto:#{MvWeb.MemberLive.Index.format_member_email(@member)}"}
class="text-blue-700 hover:text-blue-800 underline"
>
{@member.email}
</a>
</.data_field>
</div>
<%!-- Email --%>
<div>
<.data_field label={gettext("Email")}>
<a
href={"mailto:#{MvWeb.MemberLive.Index.format_member_email(@member)}"}
class="text-blue-700 hover:text-blue-800 underline"
>
{@member.email}
</a>
</.data_field>
</div>
<%!-- Membership Dates Row --%>
<div class="flex gap-6">
<.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"
/>
</div>
<%!-- Membership Dates Row --%>
<div class="flex gap-6">
<.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"
/>
</div>
<%!-- Linked User: only show when current user can see other users (e.g. admin).
<%!-- Linked User: only show when current user can see other users (e.g. admin).
read_only cannot see linked user, so hide the section to avoid "No user linked" when
a user is linked but not visible. --%>
<%= if can_access_page?(@current_user, "/users") do %>
<%= if can_access_page?(@current_user, "/users") do %>
<div>
<.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}
</.link>
<% else %>
<span class="text-base-content/70 italic">{gettext("No user linked")}</span>
<% end %>
</.data_field>
</div>
<% end %>
<%!-- Groups (in Personal Data) --%>
<% groups = @member.groups || [] %>
<div>
<.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}
</.link>
<.data_field label={gettext("Groups")}>
<%= if Enum.empty?(groups) do %>
<span class="text-base-content/70 italic">{gettext("No groups")}</span>
<% else %>
<span class="text-base-content/70 italic">{gettext("No user linked")}</span>
<div class="flex flex-wrap gap-2">
<%= for group <- groups do %>
<.button
variant="outline"
size="sm"
navigate={~p"/groups/#{group.slug}"}
aria-label={gettext("Member of group %{name}", name: group.name)}
>
{group.name}
</.button>
<% end %>
</div>
<% end %>
</.data_field>
</div>
<% end %>
<%!-- Groups (in Personal Data) --%>
<% groups = @member.groups || [] %>
<div>
<.data_field label={gettext("Groups")}>
<%= if Enum.empty?(groups) do %>
<span class="text-base-content/70 italic">{gettext("No groups")}</span>
<% else %>
<div class="flex flex-wrap gap-2">
<%= for group <- groups do %>
<.link
navigate={~p"/groups/#{group.slug}"}
class="btn btn-xs btn-outline btn-primary"
aria-label={gettext("Member of group %{name}", name: group.name)}
>
{group.name}
</.link>
<% end %>
</div>
<% end %>
</.data_field>
</div>
<%!-- Notes --%>
<%= if @member.notes && String.trim(@member.notes) != "" do %>
<div>
<.data_field label={gettext("Notes")}>
<p class="whitespace-pre-wrap text-base-content/80">{@member.notes}</p>
</.data_field>
</div>
<% end %>
</div>
</.section_box>
</div>
<%!-- Custom Fields Section --%>
<%= if Enum.any?(@custom_fields) do %>
<div>
<.section_box title={gettext("Custom Fields")}>
<div class="grid grid-cols-2 gap-4">
<%= for custom_field <- @custom_fields do %>
<% cfv = find_custom_field_value(@member.custom_field_values, custom_field.id) %>
<.data_field label={custom_field.name}>
{format_custom_field_value(cfv, custom_field.value_type)}
</.data_field>
<%!-- Notes --%>
<%= if @member.notes && String.trim(@member.notes) != "" do %>
<div>
<.data_field label={gettext("Notes")}>
<p class="whitespace-pre-wrap text-base-content/80">{@member.notes}</p>
</.data_field>
</div>
<% end %>
</div>
</.section_box>
</div>
<% end %>
</div>
<%!-- Payment Data Section --%>
<div class="w-full">
<.section_box title={gettext("Payment Data")}>
<%= if @member.membership_fee_type do %>
<div class="flex gap-6 flex-wrap">
<.data_field
label={gettext("Type")}
value={@member.membership_fee_type.name}
class="min-w-32"
/>
<.data_field
label={gettext("Membership Fee")}
value={MembershipFeeHelpers.format_currency(@member.membership_fee_type.amount)}
class="min-w-24"
/>
<.data_field
label={gettext("Payment Interval")}
value={MembershipFeeHelpers.format_interval(@member.membership_fee_type.interval)}
class="min-w-32"
/>
<.data_field label={gettext("Last Cycle")} class="min-w-32">
<%= if @member.last_cycle_status do %>
<% status = @member.last_cycle_status %>
<span class={["badge", MembershipFeeHelpers.status_color(status)]}>
{format_status_label(status)}
</span>
<% else %>
<span class="badge badge-ghost">{gettext("No cycles")}</span>
<% end %>
</.data_field>
<.data_field label={gettext("Current Cycle")} class="min-w-36">
<%= if @member.current_cycle_status do %>
<% status = @member.current_cycle_status %>
<span class={["badge", MembershipFeeHelpers.status_color(status)]}>
{format_status_label(status)}
</span>
<% else %>
<span class="badge badge-ghost">{gettext("No cycles")}</span>
<% end %>
</.data_field>
</div>
<% else %>
<div class="text-base-content/70 italic">
{gettext("No membership fee type assigned")}
<%!-- Custom Fields Section --%>
<%= if Enum.any?(@custom_fields) do %>
<div>
<.section_box title={gettext("Custom Fields")}>
<div class="grid grid-cols-2 gap-4">
<%= for custom_field <- @custom_fields do %>
<% cfv = find_custom_field_value(@member.custom_field_values, custom_field.id) %>
<.data_field label={custom_field.name}>
{format_custom_field_value(cfv, custom_field.value_type)}
</.data_field>
<% end %>
</div>
</.section_box>
</div>
<% end %>
</.section_box>
</div>
<% end %>
</div>
<%= if @active_tab == :membership_fees do %>
<%!-- Membership Fees Tab Content --%>
<.live_component
module={MvWeb.MemberLive.Show.MembershipFeesComponent}
id={"membership-fees-#{@member.id}"}
member={@member}
current_user={@current_user}
vereinfacht_receipts={@vereinfacht_receipts}
/>
<% end %>
<%!-- Payment Data Section --%>
<div class="w-full">
<.section_box title={gettext("Payment Data")}>
<%= if @member.membership_fee_type do %>
<div class="flex gap-6 flex-wrap">
<.data_field
label={gettext("Type")}
value={@member.membership_fee_type.name}
class="min-w-32"
/>
<.data_field
label={gettext("Membership Fee")}
value={MembershipFeeHelpers.format_currency(@member.membership_fee_type.amount)}
class="min-w-24"
/>
<.data_field
label={gettext("Payment Interval")}
value={MembershipFeeHelpers.format_interval(@member.membership_fee_type.interval)}
class="min-w-32"
/>
<.data_field label={gettext("Last Cycle")} class="min-w-32">
<%= if @member.last_cycle_status do %>
<% status = @member.last_cycle_status %>
<span class={["badge", MembershipFeeHelpers.status_color(status)]}>
{format_status_label(status)}
</span>
<% else %>
<span class="badge badge-ghost">{gettext("No cycles")}</span>
<% end %>
</.data_field>
<.data_field label={gettext("Current Cycle")} class="min-w-36">
<%= if @member.current_cycle_status do %>
<% status = @member.current_cycle_status %>
<span class={["badge", MembershipFeeHelpers.status_color(status)]}>
{format_status_label(status)}
</span>
<% else %>
<span class="badge badge-ghost">{gettext("No cycles")}</span>
<% end %>
</.data_field>
</div>
<% else %>
<div class="text-base-content/70 italic">
{gettext("No membership fee type assigned")}
</div>
<% end %>
</.section_box>
</div>
<% 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}
current_user={@current_user}
vereinfacht_receipts={@vereinfacht_receipts}
/>
<% end %>
</div>
</Layouts.app>
"""
end
@ -403,7 +411,7 @@ defmodule MvWeb.MemberLive.Show do
~H"""
<a
href={"mailto:#{@email}"}
class="text-blue-700 hover:text-blue-800 underline"
class="link link-primary"
>
{@display}
</a>

View file

@ -66,14 +66,15 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
<.icon name="hero-arrow-top-right-on-square" class="inline-block size-4" />
</.link>
<div>
<button
<.button
type="button"
variant="ghost"
size="sm"
phx-click="load_vereinfacht_receipts"
phx-value-contact_id={@member.vereinfacht_contact_id}
class="btn btn-sm btn-ghost"
>
{gettext("Show bookings/receipts from Vereinfacht")}
</button>
</.button>
</div>
<%= if @vereinfacht_receipts do %>
<div
@ -148,9 +149,10 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
</.button>
<.button
:if={Enum.any?(@cycles) and @can_destroy_cycle}
variant="outline"
size="sm"
phx-click="delete_all_cycles"
phx-target={@myself}
class="btn btn-sm btn-error btn-outline"
title={gettext("Delete all cycles")}
>
<.icon name="hero-trash" class="size-4" />
@ -158,9 +160,10 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
</.button>
<.button
:if={@member.membership_fee_type != nil and @can_create_cycle}
variant="primary"
size="sm"
phx-click="open_create_cycle_modal"
phx-target={@myself}
class="btn btn-sm btn-primary"
title={gettext("Create a new cycle manually")}
>
<.icon name="hero-plus" class="size-4" />
@ -259,17 +262,18 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
</div>
<% end %>
<%= if @can_destroy_cycle do %>
<button
<.button
type="button"
variant="outline"
size="sm"
phx-click="delete_cycle"
phx-value-cycle_id={cycle.id}
phx-target={@myself}
class="btn btn-sm btn-error btn-outline"
title={gettext("Delete cycle")}
>
<.icon name="hero-trash" class="size-4" />
{gettext("Delete")}
</button>
</.button>
<% end %>
</div>
</:action>
@ -309,10 +313,15 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
/>
</div>
<div class="modal-action">
<button type="button" phx-click="cancel_edit_amount" phx-target={@myself} class="btn">
<.button
type="button"
variant="neutral"
phx-click="cancel_edit_amount"
phx-target={@myself}
>
{gettext("Cancel")}
</button>
<button type="submit" class="btn btn-primary">{gettext("Save")}</button>
</.button>
<.button type="submit" variant="primary">{gettext("Save")}</.button>
</div>
</form>
</div>
@ -334,17 +343,17 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
)} - {MembershipFeeHelpers.format_currency(@deleting_cycle.amount)}
</p>
<div class="modal-action">
<button phx-click="cancel_delete_cycle" phx-target={@myself} class="btn">
<.button variant="neutral" phx-click="cancel_delete_cycle" phx-target={@myself}>
{gettext("Cancel")}
</button>
<button
</.button>
<.button
variant="danger"
phx-click="confirm_delete_cycle"
phx-value-cycle_id={@deleting_cycle.id}
phx-target={@myself}
class="btn btn-error"
>
{gettext("Delete")}
</button>
</.button>
</div>
</div>
</dialog>
@ -385,20 +394,20 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
/>
</div>
<div class="modal-action">
<button phx-click="cancel_delete_all_cycles" phx-target={@myself} class="btn">
<.button variant="neutral" phx-click="cancel_delete_all_cycles" phx-target={@myself}>
{gettext("Cancel")}
</button>
<button
</.button>
<.button
variant="danger"
phx-click="confirm_delete_all_cycles"
phx-target={@myself}
class="btn btn-error"
disabled={
String.trim(String.downcase(@delete_all_confirmation)) !=
String.downcase(gettext("Yes"))
}
>
{gettext("Delete All")}
</button>
</.button>
</div>
</div>
</dialog>
@ -472,10 +481,15 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
</div>
<% end %>
<div class="modal-action">
<button type="button" phx-click="cancel_create_cycle" phx-target={@myself} class="btn">
<.button
type="button"
variant="neutral"
phx-click="cancel_create_cycle"
phx-target={@myself}
>
{gettext("Cancel")}
</button>
<button type="submit" class="btn btn-primary">{gettext("Create")}</button>
</.button>
<.button type="submit" variant="primary">{gettext("Create")}</.button>
</div>
</form>
</div>