diff --git a/lib/mv_web/live/member_live/show/membership_fees_component.ex b/lib/mv_web/live/member_live/show/membership_fees_component.ex index fe0030a..2ccac15 100644 --- a/lib/mv_web/live/member_live/show/membership_fees_component.ex +++ b/lib/mv_web/live/member_live/show/membership_fees_component.ex @@ -16,7 +16,9 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do alias Mv.Membership alias Mv.MembershipFees.MembershipFeeType + alias Mv.MembershipFees.MembershipFeeCycle alias Mv.MembershipFees.CycleGenerator + alias Mv.MembershipFees.CalendarCycles alias MvWeb.Helpers.MembershipFeeHelpers @impl true @@ -56,6 +58,26 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do <.icon name="hero-arrow-path" class="size-4" /> {if(@regenerating, do: gettext("Regenerating..."), else: gettext("Regenerate Cycles"))} + <.button + :if={Enum.any?(@cycles)} + 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" /> + {gettext("Delete All Cycles")} + + <.button + :if={@member.membership_fee_type} + 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" /> + {gettext("Create Cycle")} + <%!-- Cycles Table --%> @@ -79,7 +101,15 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do <:col :let={cycle} label={gettext("Amount")}> - {MembershipFeeHelpers.format_currency(cycle.amount)} + + {MembershipFeeHelpers.format_currency(cycle.amount)} + <:col :let={cycle} label={gettext("Status")}> @@ -92,80 +122,59 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do <:action :let={cycle}> - @@ -243,6 +252,130 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do <% end %> + + <%!-- Delete All Cycles Confirmation Modal --%> + <%= if @deleting_all_cycles do %> + + + + <% end %> + + <%!-- Create Cycle Modal --%> + <%= if @creating_cycle do %> + + + + <% end %> """ end @@ -273,6 +406,11 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do |> assign_new(:interval_warning, fn -> nil end) |> assign_new(:editing_cycle, fn -> nil end) |> assign_new(:deleting_cycle, fn -> nil end) + |> assign_new(:deleting_all_cycles, fn -> false end) + |> assign_new(:delete_all_confirmation, fn -> "" end) + |> assign_new(:creating_cycle, fn -> false end) + |> assign_new(:create_cycle_date, fn -> nil end) + |> assign_new(:create_cycle_error, fn -> nil end) |> assign_new(:regenerating, fn -> false end)} end @@ -509,6 +647,173 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do end end + def handle_event("delete_all_cycles", _params, socket) do + {:noreply, + socket + |> assign(:deleting_all_cycles, true) + |> assign(:delete_all_confirmation, "")} + end + + def handle_event("cancel_delete_all_cycles", _params, socket) do + {:noreply, + socket + |> assign(:deleting_all_cycles, false) + |> assign(:delete_all_confirmation, "")} + end + + def handle_event("update_delete_all_confirmation", %{"value" => value}, socket) do + {:noreply, assign(socket, :delete_all_confirmation, value)} + end + + def handle_event("confirm_delete_all_cycles", _params, socket) do + member = socket.assigns.member + cycles = socket.assigns.cycles + + # Delete all cycles + results = + Enum.map(cycles, fn cycle -> + Ash.destroy(cycle) + end) + + # Check if all deletions were successful + errors = Enum.filter(results, &match?({:error, _}, &1)) + + if Enum.empty?(errors) do + # Reload member to get updated cycles + updated_member = + member + |> Ash.load!([ + :membership_fee_type, + membership_fee_cycles: [:membership_fee_type] + ]) + + updated_cycles = + Enum.sort_by( + updated_member.membership_fee_cycles || [], + & &1.cycle_start, + {:desc, Date} + ) + + send(self(), {:member_updated, updated_member}) + + {:noreply, + socket + |> assign(:member, updated_member) + |> assign(:cycles, updated_cycles) + |> assign(:deleting_all_cycles, false) + |> assign(:delete_all_confirmation, "") + |> put_flash(:info, gettext("All cycles deleted"))} + else + error_msg = + Enum.map_join(errors, ", ", fn {:error, error} -> format_error(error) end) + + {:noreply, + socket + |> assign(:deleting_all_cycles, false) + |> assign(:delete_all_confirmation, "") + |> put_flash(:error, gettext("Failed to delete some cycles: %{errors}", errors: error_msg))} + end + end + + def handle_event("open_create_cycle_modal", _params, socket) do + {:noreply, + socket + |> assign(:creating_cycle, true) + |> assign(:create_cycle_date, nil) + |> assign(:create_cycle_error, nil)} + end + + def handle_event("cancel_create_cycle", _params, socket) do + {:noreply, + socket + |> assign(:creating_cycle, false) + |> assign(:create_cycle_date, nil) + |> assign(:create_cycle_error, nil)} + end + + def handle_event("update_create_cycle_date", %{"value" => date_str}, socket) do + date = + case Date.from_iso8601(date_str) do + {:ok, date} -> date + _ -> nil + end + + {:noreply, + socket + |> assign(:create_cycle_date, date) + |> assign(:create_cycle_error, nil)} + end + + def handle_event("create_cycle", %{"date" => date_str, "amount" => amount_str}, socket) do + member = socket.assigns.member + + with {:ok, date} <- Date.from_iso8601(date_str), + {amount, _} when is_struct(amount, Decimal) <- Decimal.parse(amount_str), + cycle_start <- + CalendarCycles.calculate_cycle_start(date, member.membership_fee_type.interval), + :ok <- validate_cycle_not_exists(socket.assigns.cycles, cycle_start) do + attrs = %{ + cycle_start: cycle_start, + amount: amount, + status: :unpaid, + member_id: member.id, + membership_fee_type_id: member.membership_fee_type_id + } + + case Ash.create(MembershipFeeCycle, attrs) do + {:ok, _new_cycle} -> + # Reload member with cycles + updated_member = + member + |> Ash.load!([ + :membership_fee_type, + membership_fee_cycles: [:membership_fee_type] + ]) + + cycles = + Enum.sort_by( + updated_member.membership_fee_cycles || [], + & &1.cycle_start, + {:desc, Date} + ) + + send(self(), {:member_updated, updated_member}) + + {:noreply, + socket + |> assign(:member, updated_member) + |> assign(:cycles, cycles) + |> assign(:creating_cycle, false) + |> assign(:create_cycle_date, nil) + |> assign(:create_cycle_error, nil) + |> put_flash(:info, gettext("Cycle created successfully"))} + + {:error, error} -> + {:noreply, + socket + |> assign(:create_cycle_error, format_error(error))} + end + else + :error -> + {:noreply, + socket + |> assign(:create_cycle_error, gettext("Invalid date format"))} + + {:error, :invalid_amount} -> + {:noreply, + socket + |> assign(:create_cycle_error, gettext("Invalid amount format"))} + + {:error, :cycle_exists} -> + {:noreply, + socket + |> assign( + :create_cycle_error, + gettext("A cycle for this period already exists") + )} + end + end + # Helper functions defp get_available_fee_types(member) do @@ -559,6 +864,24 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do defp format_error(error) when is_binary(error), do: error defp format_error(_error), do: gettext("An error occurred") + defp validate_cycle_not_exists(cycles, cycle_start) do + if Enum.any?(cycles, &(&1.cycle_start == cycle_start)) do + {:error, :cycle_exists} + else + :ok + end + end + + defp format_create_cycle_period(date, interval) when is_struct(date, Date) do + cycle_start = CalendarCycles.calculate_cycle_start(date, interval) + cycle_end = CalendarCycles.calculate_cycle_end(cycle_start, interval) + + MembershipFeeHelpers.format_cycle_range(cycle_start, interval) <> + " (#{Calendar.strftime(cycle_start, "%d.%m.%Y")} - #{Calendar.strftime(cycle_end, "%d.%m.%Y")})" + end + + defp format_create_cycle_period(_date, _interval), do: "" + # Helper component for section box attr :title, :string, required: true slot :inner_block, required: true