Membership Fee 6 - UI Components & LiveViews closes #280 #304

Open
moritz wants to merge 65 commits from feature/280_membership_fee_ui into main
Showing only changes of commit 35cafd6e6a - Show all commits

View file

@ -21,6 +21,10 @@ defmodule MvWeb.MemberLive.Form do
""" """
use MvWeb, :live_view use MvWeb, :live_view
alias Mv.MembershipFees
alias Mv.MembershipFees.MembershipFeeType
alias MvWeb.Helpers.MembershipFeeHelpers
@impl true @impl true
def render(assigns) do def render(assigns) do
# Sort custom fields by name for display only # Sort custom fields by name for display only
@ -161,42 +165,46 @@ defmodule MvWeb.MemberLive.Form do
<% end %> <% end %>
</div> </div>
<%!-- Payment Data Section (Mockup) --%> <%!-- Membership Fee Section --%>
<div class="max-w-xl"> <div class="max-w-xl">
<.form_section title={gettext("Payment Data")}> <.form_section title={gettext("Membership Fee")}>
<div role="alert" class="alert alert-info mb-4"> <div class="space-y-4">
<.icon name="hero-information-circle" class="size-5" /> <div>
<span>{gettext("This data is for demonstration purposes only (mockup).")}</span> <label class="label">
</div> <span class="label-text font-semibold">{gettext("Membership Fee Type")}</span>
<div class="flex gap-8">
<div class="w-24">
<label for="mock-contribution" class="label text-sm font-medium">
{gettext("Contribution")}
</label> </label>
<input <select
type="text" class="select select-bordered w-full"
id="mock-contribution" name={@form[:membership_fee_type_id].name}
value="72 €" phx-change="validate_membership_fee_type"
disabled value={@form[:membership_fee_type_id].value || ""}
class="input input-bordered w-full bg-base-200" >
/> <option value="">{gettext("None")}</option>
</div> <%= for fee_type <- @available_fee_types do %>
<div class="w-40"> <option
<label class="label text-sm font-medium">{gettext("Payment Cycle")}</label> value={fee_type.id}
<div class="flex gap-3 mt-2"> selected={fee_type.id == @form[:membership_fee_type_id].value}
<label class="flex items-center gap-1 cursor-not-allowed opacity-60"> >
<input type="radio" name="mock_cycle" checked disabled class="radio radio-sm" /> {fee_type.name} ({MembershipFeeHelpers.format_currency(fee_type.amount)}, {MembershipFeeHelpers.format_interval(
<span class="text-sm">{gettext("monthly")}</span> fee_type.interval
</label> )})
<label class="flex items-center gap-1 cursor-not-allowed opacity-60"> </option>
<input type="radio" name="mock_cycle" disabled class="radio radio-sm" /> <% end %>
<span class="text-sm">{gettext("yearly")}</span> </select>
</label> <%= for {msg, _opts} <- @form.errors[:membership_fee_type_id] || [] do %>
</div> <p class="text-error text-sm mt-1">{msg}</p>
</div> <% end %>
<div class="w-24 flex items-end"> <%= if @interval_warning do %>
<.input field={@form[:paid]} label={gettext("Paid")} type="checkbox" /> <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>
</div> </div>
</.form_section> </.form_section>
@ -235,12 +243,15 @@ defmodule MvWeb.MemberLive.Form do
member = member =
case params["id"] do case params["id"] do
nil -> nil nil -> nil
id -> Ash.get!(Mv.Membership.Member, id) id -> Ash.get!(Mv.Membership.Member, id, load: [:membership_fee_type])
end end
page_title = page_title =
if is_nil(member), do: gettext("Create Member"), else: gettext("Edit Member") if is_nil(member), do: gettext("Create Member"), else: gettext("Edit Member")
# Load available membership fee types
available_fee_types = load_available_fee_types(member)
{:ok, {:ok,
socket socket
|> assign(:return_to, return_to(params["return_to"])) |> assign(:return_to, return_to(params["return_to"]))
@ -248,6 +259,8 @@ defmodule MvWeb.MemberLive.Form do
|> assign(:initial_custom_field_values, initial_custom_field_values) |> assign(:initial_custom_field_values, initial_custom_field_values)
|> assign(member: member) |> assign(member: member)
|> assign(:page_title, page_title) |> assign(:page_title, page_title)
|> assign(:available_fee_types, available_fee_types)
|> assign(:interval_warning, nil)
|> assign_form()} |> assign_form()}
end end
@ -256,7 +269,53 @@ defmodule MvWeb.MemberLive.Form do
@impl true @impl true
def handle_event("validate", %{"member" => member_params}, socket) do def handle_event("validate", %{"member" => member_params}, socket) do
{:noreply, assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, member_params))} validated_form = AshPhoenix.Form.validate(socket.assigns.form, member_params)
# Check for interval mismatch if membership_fee_type_id changed
socket =
if Map.has_key?(member_params, "membership_fee_type_id") &&
socket.assigns.member &&
socket.assigns.member.membership_fee_type do
new_fee_type_id = member_params["membership_fee_type_id"]
if new_fee_type_id != "" &&
new_fee_type_id != socket.assigns.member.membership_fee_type_id do
new_fee_type = find_fee_type(socket.assigns.available_fee_types, new_fee_type_id)
if new_fee_type &&
new_fee_type.interval != socket.assigns.member.membership_fee_type.interval do
assign(
socket,
:interval_warning,
gettext(
"Warning: Changing from %{old_interval} to %{new_interval} is not allowed. Please select a membership fee type with the same interval.",
old_interval:
MembershipFeeHelpers.format_interval(
socket.assigns.member.membership_fee_type.interval
),
new_interval: MembershipFeeHelpers.format_interval(new_fee_type.interval)
)
)
else
assign(socket, :interval_warning, nil)
end
else
assign(socket, :interval_warning, nil)
end
else
socket
end
{:noreply, assign(socket, form: validated_form)}
end
def handle_event(
"validate_membership_fee_type",
%{"member" => %{"membership_fee_type_id" => fee_type_id}},
socket
) do
# Same validation as above, but triggered by select change
handle_event("validate", %{"member" => %{"membership_fee_type_id" => fee_type_id}}, socket)
end end
def handle_event("save", %{"member" => member_params}, socket) do def handle_event("save", %{"member" => member_params}, socket) do
@ -348,6 +407,30 @@ defmodule MvWeb.MemberLive.Form do
defp return_path("show", nil), do: ~p"/members" defp return_path("show", nil), do: ~p"/members"
defp return_path("show", member), do: ~p"/members/#{member.id}" defp return_path("show", member), do: ~p"/members/#{member.id}"
# -----------------------------------------------------------------
# Helper Functions
# -----------------------------------------------------------------
defp load_available_fee_types(member) do
all_types =
MembershipFeeType
|> Ash.Query.sort(name: :asc)
|> Ash.read!(domain: MembershipFees)
# If member has a fee type, filter to same interval
if member && member.membership_fee_type do
Enum.filter(all_types, fn type ->
type.interval == member.membership_fee_type.interval
end)
else
all_types
end
end
defp find_fee_type(fee_types, fee_type_id) do
Enum.find(fee_types, &(&1.id == fee_type_id))
end
# ----------------------------------------------------------------- # -----------------------------------------------------------------
# Helper Functions for Custom Fields # Helper Functions for Custom Fields
# ----------------------------------------------------------------- # -----------------------------------------------------------------