defmodule MvWeb.Helpers.MembershipFeeHelpers do @moduledoc """ Helper functions for membership fee UI components. Provides formatting and utility functions for displaying membership fee information in LiveViews and templates. """ use Gettext, backend: MvWeb.Gettext alias Mv.MembershipFees.CalendarCycles alias Mv.MembershipFees.MembershipFeeCycle alias Mv.Membership.Member @doc """ Formats a decimal amount as currency string. ## Examples iex> MvWeb.Helpers.MembershipFeeHelpers.format_currency(Decimal.new("60.00")) "60,00 €" iex> MvWeb.Helpers.MembershipFeeHelpers.format_currency(Decimal.new("5.5")) "5,50 €" """ @spec format_currency(Decimal.t()) :: String.t() def format_currency(%Decimal{} = amount) do # Use German format: comma as decimal separator, always 2 decimal places # Normalize to 2 decimal places normalized = Decimal.round(amount, 2) normalized_str = normalized |> Decimal.to_string(:normal) |> String.replace(".", ",") # Ensure 2 decimal places case String.split(normalized_str, ",") do [int_part, dec_part] when byte_size(dec_part) == 1 -> "#{int_part},#{dec_part}0 €" [int_part, dec_part] when byte_size(dec_part) == 2 -> "#{int_part},#{dec_part} €" [int_part] -> "#{int_part},00 €" _ -> "#{normalized_str} €" end end @doc """ Formats an interval atom as a translated string. ## Examples iex> MvWeb.Helpers.MembershipFeeHelpers.format_interval(:monthly) "Monthly" iex> MvWeb.Helpers.MembershipFeeHelpers.format_interval(:yearly) "Yearly" """ @spec format_interval(:monthly | :quarterly | :half_yearly | :yearly) :: String.t() def format_interval(:monthly), do: gettext("Monthly") def format_interval(:quarterly), do: gettext("Quarterly") def format_interval(:half_yearly), do: gettext("Half-yearly") def format_interval(:yearly), do: gettext("Yearly") @doc """ Formats a cycle date range as a string. Calculates the cycle end date from cycle_start and interval, then formats both dates in European format (dd.mm.yyyy). ## Examples iex> cycle_start = ~D[2024-01-01] iex> MvWeb.Helpers.MembershipFeeHelpers.format_cycle_range(cycle_start, :yearly) "01.01.2024 - 31.12.2024" iex> cycle_start = ~D[2024-03-01] iex> MvWeb.Helpers.MembershipFeeHelpers.format_cycle_range(cycle_start, :monthly) "01.03.2024 - 31.03.2024" """ @spec format_cycle_range(Date.t(), :monthly | :quarterly | :half_yearly | :yearly) :: String.t() def format_cycle_range(cycle_start, interval) do cycle_end = CalendarCycles.calculate_cycle_end(cycle_start, interval) start_str = format_date(cycle_start) end_str = format_date(cycle_end) "#{start_str} - #{end_str}" end @doc """ Gets the last completed cycle for a member. Returns the cycle that was most recently completed (ended before today). Returns `nil` if no completed cycles exist. ## Parameters - `member` - Member struct with loaded membership_fee_cycles and membership_fee_type - `today` - Optional date to use as reference (defaults to today) ## Returns - `%MembershipFeeCycle{}` if found - `nil` if no completed cycle exists ## Examples # Member with cycles from 2023 and 2024, today is 2025-01-15 iex> cycle = MvWeb.Helpers.MembershipFeeHelpers.get_last_completed_cycle(member) # => %MembershipFeeCycle{cycle_start: ~D[2024-01-01], ...} """ @spec get_last_completed_cycle(Member.t(), Date.t() | nil) :: MembershipFeeCycle.t() | nil def get_last_completed_cycle(member, today \\ nil) def get_last_completed_cycle(%Member{} = member, today) do today = today || Date.utc_today() case member.membership_fee_type do nil -> nil fee_type -> cycles = member.membership_fee_cycles || [] cycles |> Enum.filter(fn cycle -> CalendarCycles.last_completed_cycle?(cycle.cycle_start, fee_type.interval, today) end) |> List.first() end end @doc """ Gets the current cycle for a member. Returns the cycle that contains today's date. Returns `nil` if no current cycle exists. ## Parameters - `member` - Member struct with loaded membership_fee_cycles and membership_fee_type - `today` - Optional date to use as reference (defaults to today) ## Returns - `%MembershipFeeCycle{}` if found - `nil` if no current cycle exists ## Examples # Member with cycles, today is 2024-06-15 (within Q2 2024) iex> cycle = MvWeb.Helpers.MembershipFeeHelpers.get_current_cycle(member) # => %MembershipFeeCycle{cycle_start: ~D[2024-04-01], ...} """ @spec get_current_cycle(Member.t(), Date.t() | nil) :: MembershipFeeCycle.t() | nil def get_current_cycle(member, today \\ nil) def get_current_cycle(%Member{} = member, today) do today = today || Date.utc_today() case member.membership_fee_type do nil -> nil fee_type -> cycles = member.membership_fee_cycles || [] cycles |> Enum.filter(fn cycle -> CalendarCycles.current_cycle?(cycle.cycle_start, fee_type.interval, today) end) |> List.first() end end @doc """ Gets the CSS color class for a status badge. ## Examples iex> MvWeb.Helpers.MembershipFeeHelpers.status_color(:paid) "badge-success" iex> MvWeb.Helpers.MembershipFeeHelpers.status_color(:unpaid) "badge-error" iex> MvWeb.Helpers.MembershipFeeHelpers.status_color(:suspended) "badge-ghost" """ @spec status_color(:paid | :unpaid | :suspended) :: String.t() def status_color(:paid), do: "badge-success" def status_color(:unpaid), do: "badge-error" def status_color(:suspended), do: "badge-ghost" @doc """ Gets the icon name for a status. ## Examples iex> MvWeb.Helpers.MembershipFeeHelpers.status_icon(:paid) "hero-check-circle" iex> MvWeb.Helpers.MembershipFeeHelpers.status_icon(:unpaid) "hero-x-circle" iex> MvWeb.Helpers.MembershipFeeHelpers.status_icon(:suspended) "hero-pause-circle" """ @spec status_icon(:paid | :unpaid | :suspended) :: String.t() def status_icon(:paid), do: "hero-check-circle" def status_icon(:unpaid), do: "hero-x-circle" def status_icon(:suspended), do: "hero-pause-circle" # Private helper function for date formatting defp format_date(%Date{} = date) do Calendar.strftime(date, "%d.%m.%Y") end end