Membership Fee 6 - UI Components & LiveViews closes #280 #304
10 changed files with 490 additions and 247 deletions
|
|
@ -2,11 +2,12 @@ defmodule MvWeb.Components.PaymentFilterComponent do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Provides the PaymentFilter Live-Component.
|
Provides the PaymentFilter Live-Component.
|
||||||
|
|
||||||
A dropdown filter for filtering members by payment status (paid/not paid/all).
|
A dropdown filter for filtering members by cycle payment status (paid/unpaid/all).
|
||||||
Uses DaisyUI dropdown styling and sends filter changes to parent LiveView.
|
Uses DaisyUI dropdown styling and sends filter changes to parent LiveView.
|
||||||
|
Filter is based on cycle status (last or current cycle, depending on cycle view toggle).
|
||||||
|
|
||||||
## Props
|
## Props
|
||||||
- `:paid_filter` - Current filter state: `nil` (all), `:paid`, or `:not_paid`
|
- `:cycle_status_filter` - Current filter state: `nil` (all), `:paid`, or `:unpaid`
|
||||||
- `:id` - Component ID (required)
|
- `:id` - Component ID (required)
|
||||||
- `:member_count` - Number of filtered members to display in badge (optional, default: 0)
|
- `:member_count` - Number of filtered members to display in badge (optional, default: 0)
|
||||||
|
|
||||||
|
|
@ -25,7 +26,7 @@ defmodule MvWeb.Components.PaymentFilterComponent do
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> assign(:id, assigns.id)
|
|> assign(:id, assigns.id)
|
||||||
|> assign(:paid_filter, assigns[:paid_filter])
|
|> assign(:cycle_status_filter, assigns[:cycle_status_filter])
|
||||||
|> assign(:member_count, assigns[:member_count] || 0)
|
|> assign(:member_count, assigns[:member_count] || 0)
|
||||||
|
|
||||||
{:ok, socket}
|
{:ok, socket}
|
||||||
|
|
@ -45,7 +46,7 @@ defmodule MvWeb.Components.PaymentFilterComponent do
|
||||||
type="button"
|
type="button"
|
||||||
class={[
|
class={[
|
||||||
"btn gap-2",
|
"btn gap-2",
|
||||||
@paid_filter && "btn-active"
|
@cycle_status_filter && "btn-active"
|
||||||
]}
|
]}
|
||||||
phx-click="toggle_dropdown"
|
phx-click="toggle_dropdown"
|
||||||
phx-target={@myself}
|
phx-target={@myself}
|
||||||
|
|
@ -54,8 +55,8 @@ defmodule MvWeb.Components.PaymentFilterComponent do
|
||||||
aria-label={gettext("Filter by payment status")}
|
aria-label={gettext("Filter by payment status")}
|
||||||
>
|
>
|
||||||
<.icon name="hero-funnel" class="h-5 w-5" />
|
<.icon name="hero-funnel" class="h-5 w-5" />
|
||||||
<span class="hidden sm:inline">{filter_label(@paid_filter)}</span>
|
<span class="hidden sm:inline">{filter_label(@cycle_status_filter)}</span>
|
||||||
<span :if={@paid_filter} class="badge badge-primary badge-sm">{@member_count}</span>
|
<span :if={@cycle_status_filter} class="badge badge-primary badge-sm">{@member_count}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul
|
<ul
|
||||||
|
|
@ -70,22 +71,22 @@ defmodule MvWeb.Components.PaymentFilterComponent do
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
role="menuitemradio"
|
role="menuitemradio"
|
||||||
aria-checked={to_string(@paid_filter == nil)}
|
aria-checked={to_string(@cycle_status_filter == nil)}
|
||||||
class={@paid_filter == nil && "active"}
|
class={@cycle_status_filter == nil && "active"}
|
||||||
phx-click="select_filter"
|
phx-click="select_filter"
|
||||||
phx-value-filter=""
|
phx-value-filter=""
|
||||||
phx-target={@myself}
|
phx-target={@myself}
|
||||||
>
|
>
|
||||||
<.icon name="hero-users" class="h-4 w-4" />
|
<.icon name="hero-users" class="h-4 w-4" />
|
||||||
{gettext("All payment statuses")}
|
{gettext("All")}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li role="none">
|
<li role="none">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
role="menuitemradio"
|
role="menuitemradio"
|
||||||
aria-checked={to_string(@paid_filter == :paid)}
|
aria-checked={to_string(@cycle_status_filter == :paid)}
|
||||||
class={@paid_filter == :paid && "active"}
|
class={@cycle_status_filter == :paid && "active"}
|
||||||
phx-click="select_filter"
|
phx-click="select_filter"
|
||||||
phx-value-filter="paid"
|
phx-value-filter="paid"
|
||||||
phx-target={@myself}
|
phx-target={@myself}
|
||||||
|
|
@ -98,14 +99,14 @@ defmodule MvWeb.Components.PaymentFilterComponent do
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
role="menuitemradio"
|
role="menuitemradio"
|
||||||
aria-checked={to_string(@paid_filter == :not_paid)}
|
aria-checked={to_string(@cycle_status_filter == :unpaid)}
|
||||||
class={@paid_filter == :not_paid && "active"}
|
class={@cycle_status_filter == :unpaid && "active"}
|
||||||
phx-click="select_filter"
|
phx-click="select_filter"
|
||||||
phx-value-filter="not_paid"
|
phx-value-filter="unpaid"
|
||||||
phx-target={@myself}
|
phx-target={@myself}
|
||||||
>
|
>
|
||||||
<.icon name="hero-x-circle" class="h-4 w-4 text-error" />
|
<.icon name="hero-x-circle" class="h-4 w-4 text-error" />
|
||||||
{gettext("Not paid")}
|
{gettext("Unpaid")}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -136,11 +137,11 @@ defmodule MvWeb.Components.PaymentFilterComponent do
|
||||||
|
|
||||||
# Parse filter string to atom
|
# Parse filter string to atom
|
||||||
defp parse_filter("paid"), do: :paid
|
defp parse_filter("paid"), do: :paid
|
||||||
defp parse_filter("not_paid"), do: :not_paid
|
defp parse_filter("unpaid"), do: :unpaid
|
||||||
defp parse_filter(_), do: nil
|
defp parse_filter(_), do: nil
|
||||||
|
|
||||||
# Get display label for current filter
|
# Get display label for current filter
|
||||||
defp filter_label(nil), do: gettext("All payment statuses")
|
defp filter_label(nil), do: gettext("All")
|
||||||
defp filter_label(:paid), do: gettext("Paid")
|
defp filter_label(:paid), do: gettext("Paid")
|
||||||
defp filter_label(:not_paid), do: gettext("Not paid")
|
defp filter_label(:unpaid), do: gettext("Unpaid")
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
|> assign(:query, "")
|
|> assign(:query, "")
|
||||||
|> assign_new(:sort_field, fn -> :first_name end)
|
|> assign_new(:sort_field, fn -> :first_name end)
|
||||||
|> assign_new(:sort_order, fn -> :asc end)
|
|> assign_new(:sort_order, fn -> :asc end)
|
||||||
|> assign(:paid_filter, nil)
|
|> assign(:cycle_status_filter, nil)
|
||||||
|> assign(:selected_members, MapSet.new())
|
|> assign(:selected_members, MapSet.new())
|
||||||
|> assign(:settings, settings)
|
|> assign(:settings, settings)
|
||||||
|> assign(:custom_fields_visible, custom_fields_visible)
|
|> assign(:custom_fields_visible, custom_fields_visible)
|
||||||
|
|
@ -179,27 +179,17 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
socket
|
socket
|
||||||
|> assign(:show_current_cycle, new_show_current)
|
|> assign(:show_current_cycle, new_show_current)
|
||||||
|> load_members()
|
|> load_members()
|
||||||
|
|> update_selection_assigns()
|
||||||
|
|
||||||
{:noreply, socket}
|
# Update URL to reflect cycle view change
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_event("filter_unpaid_cycles", %{"filter" => filter_str}, socket) do
|
|
||||||
filter = determine_membership_fee_filter(filter_str)
|
|
||||||
|
|
||||||
socket =
|
|
||||||
socket
|
|
||||||
|> assign(:membership_fee_status_filter, filter)
|
|
||||||
|> load_members()
|
|
||||||
|
|
||||||
query_params =
|
query_params =
|
||||||
build_query_params(
|
build_query_params(
|
||||||
socket.assigns.query,
|
socket.assigns.query,
|
||||||
socket.assigns.sort_field,
|
socket.assigns.sort_field,
|
||||||
socket.assigns.sort_order,
|
socket.assigns.sort_order,
|
||||||
socket.assigns.paid_filter
|
socket.assigns.cycle_status_filter,
|
||||||
|
new_show_current
|
||||||
)
|
)
|
||||||
|> maybe_add_membership_fee_filter(filter)
|
|
||||||
|
|
||||||
new_path = ~p"/members?#{query_params}"
|
new_path = ~p"/members?#{query_params}"
|
||||||
|
|
||||||
|
|
@ -293,8 +283,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
q,
|
q,
|
||||||
existing_field_query,
|
existing_field_query,
|
||||||
existing_sort_query,
|
existing_sort_query,
|
||||||
socket.assigns.paid_filter,
|
socket.assigns.cycle_status_filter,
|
||||||
socket.assigns.membership_fee_status_filter,
|
|
||||||
socket.assigns.show_current_cycle
|
socket.assigns.show_current_cycle
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -313,7 +302,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
def handle_info({:payment_filter_changed, filter}, socket) do
|
def handle_info({:payment_filter_changed, filter}, socket) do
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> assign(:paid_filter, filter)
|
|> assign(:cycle_status_filter, filter)
|
||||||
|> load_members()
|
|> load_members()
|
||||||
|> update_selection_assigns()
|
|> update_selection_assigns()
|
||||||
|
|
||||||
|
|
@ -324,7 +313,6 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
socket.assigns.sort_field,
|
socket.assigns.sort_field,
|
||||||
socket.assigns.sort_order,
|
socket.assigns.sort_order,
|
||||||
filter,
|
filter,
|
||||||
socket.assigns.membership_fee_status_filter,
|
|
||||||
socket.assigns.show_current_cycle
|
socket.assigns.show_current_cycle
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -439,9 +427,8 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
socket
|
socket
|
||||||
|> maybe_update_search(params)
|
|> maybe_update_search(params)
|
||||||
|> maybe_update_sort(params)
|
|> maybe_update_sort(params)
|
||||||
|> maybe_update_paid_filter(params)
|
|> maybe_update_cycle_status_filter(params)
|
||||||
|> maybe_update_show_current_cycle(params)
|
|> maybe_update_show_current_cycle(params)
|
||||||
|> maybe_update_membership_fee_status_filter(params)
|
|
||||||
|> assign(:query, params["query"])
|
|> assign(:query, params["query"])
|
||||||
|> assign(:user_field_selection, final_selection)
|
|> assign(:user_field_selection, final_selection)
|
||||||
|> assign(:member_fields_visible, visible_member_fields)
|
|> assign(:member_fields_visible, visible_member_fields)
|
||||||
|
|
@ -550,8 +537,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
socket.assigns.query,
|
socket.assigns.query,
|
||||||
field_str,
|
field_str,
|
||||||
Atom.to_string(order),
|
Atom.to_string(order),
|
||||||
socket.assigns.paid_filter,
|
socket.assigns.cycle_status_filter,
|
||||||
socket.assigns.membership_fee_status_filter,
|
|
||||||
socket.assigns.show_current_cycle
|
socket.assigns.show_current_cycle
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -581,8 +567,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
socket.assigns.query,
|
socket.assigns.query,
|
||||||
socket.assigns.sort_field,
|
socket.assigns.sort_field,
|
||||||
socket.assigns.sort_order,
|
socket.assigns.sort_order,
|
||||||
socket.assigns.paid_filter,
|
socket.assigns.cycle_status_filter,
|
||||||
socket.assigns.membership_fee_status_filter,
|
|
||||||
socket.assigns.show_current_cycle
|
socket.assigns.show_current_cycle
|
||||||
)
|
)
|
||||||
|> maybe_add_field_selection(socket.assigns[:user_field_selection])
|
|> maybe_add_field_selection(socket.assigns[:user_field_selection])
|
||||||
|
|
@ -600,14 +585,13 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Builds URL query parameters map including all filter/sort state.
|
# Builds URL query parameters map including all filter/sort state.
|
||||||
# Converts paid_filter atom to string for URL.
|
# Converts cycle_status_filter atom to string for URL.
|
||||||
defp build_query_params(
|
defp build_query_params(
|
||||||
query,
|
query,
|
||||||
sort_field,
|
sort_field,
|
||||||
sort_order,
|
sort_order,
|
||||||
paid_filter,
|
cycle_status_filter,
|
||||||
membership_fee_filter \\ nil,
|
show_current_cycle
|
||||||
show_current_cycle \\ false
|
|
||||||
) do
|
) do
|
||||||
field_str =
|
field_str =
|
||||||
if is_atom(sort_field) do
|
if is_atom(sort_field) do
|
||||||
|
|
@ -629,17 +613,14 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
"sort_order" => order_str
|
"sort_order" => order_str
|
||||||
}
|
}
|
||||||
|
|
||||||
# Only add paid_filter to URL if it's set
|
# Only add cycle_status_filter to URL if it's set
|
||||||
base_params =
|
base_params =
|
||||||
case paid_filter do
|
case cycle_status_filter do
|
||||||
nil -> base_params
|
nil -> base_params
|
||||||
:paid -> Map.put(base_params, "paid_filter", "paid")
|
:paid -> Map.put(base_params, "cycle_status_filter", "paid")
|
||||||
:not_paid -> Map.put(base_params, "paid_filter", "not_paid")
|
:unpaid -> Map.put(base_params, "cycle_status_filter", "unpaid")
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add membership fee filter if set
|
|
||||||
base_params = maybe_add_membership_fee_filter(base_params, membership_fee_filter)
|
|
||||||
|
|
||||||
# Add show_current_cycle if true
|
# Add show_current_cycle if true
|
||||||
if show_current_cycle do
|
if show_current_cycle do
|
||||||
Map.put(base_params, "show_current_cycle", "true")
|
Map.put(base_params, "show_current_cycle", "true")
|
||||||
|
|
@ -685,9 +666,6 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
# Apply the search filter first
|
# Apply the search filter first
|
||||||
query = apply_search_filter(query, search_query)
|
query = apply_search_filter(query, search_query)
|
||||||
|
|
||||||
# Apply payment status filter
|
|
||||||
query = apply_paid_filter(query, socket.assigns.paid_filter)
|
|
||||||
|
|
||||||
# Apply sorting based on current socket state
|
# Apply sorting based on current socket state
|
||||||
# For custom fields, we sort after loading
|
# For custom fields, we sort after loading
|
||||||
{query, sort_after_load} =
|
{query, sort_after_load} =
|
||||||
|
|
@ -705,11 +683,11 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
# Custom field values are already filtered at the database level in load_custom_field_values/2
|
# Custom field values are already filtered at the database level in load_custom_field_values/2
|
||||||
# No need for in-memory filtering anymore
|
# No need for in-memory filtering anymore
|
||||||
|
|
||||||
# Apply membership fee status filter if set
|
# Apply cycle status filter if set
|
||||||
members =
|
members =
|
||||||
apply_membership_fee_status_filter(
|
apply_cycle_status_filter(
|
||||||
members,
|
members,
|
||||||
socket.assigns.membership_fee_status_filter,
|
socket.assigns.cycle_status_filter,
|
||||||
socket.assigns.show_current_cycle
|
socket.assigns.show_current_cycle
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -770,22 +748,17 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Applies payment status filter to the query.
|
# Applies cycle status filter to members list.
|
||||||
#
|
#
|
||||||
# Filter values:
|
# Filter values:
|
||||||
# - nil: No filter, return all members
|
# - nil: No filter, return all members
|
||||||
# - :paid: Only members with paid == true
|
# - :paid: Only members with paid status in the selected cycle (last or current)
|
||||||
# - :not_paid: Members with paid == false or paid == nil (not paid)
|
# - :unpaid: Only members with unpaid status in the selected cycle (last or current)
|
||||||
defp apply_paid_filter(query, nil), do: query
|
defp apply_cycle_status_filter(members, nil, _show_current), do: members
|
||||||
|
|
||||||
defp apply_paid_filter(query, :paid) do
|
defp apply_cycle_status_filter(members, status, show_current)
|
||||||
Ash.Query.filter(query, expr(paid == true))
|
when status in [:paid, :unpaid] do
|
||||||
end
|
MembershipFeeStatus.filter_members_by_cycle_status(members, status, show_current)
|
||||||
|
|
||||||
defp apply_paid_filter(query, :not_paid) do
|
|
||||||
# Include both false and nil as "not paid"
|
|
||||||
# Note: paid != true doesn't work correctly with NULL values in SQL
|
|
||||||
Ash.Query.filter(query, expr(paid == false or is_nil(paid)))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Functions to toggle sorting order
|
# Functions to toggle sorting order
|
||||||
|
|
@ -1090,28 +1063,27 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
socket
|
socket
|
||||||
end
|
end
|
||||||
|
|
||||||
# Updates paid filter from URL parameters if present.
|
# Updates cycle status filter from URL parameters if present.
|
||||||
#
|
#
|
||||||
# Validates the filter value, falling back to nil (no filter) if invalid.
|
# Validates the filter value, falling back to nil (no filter) if invalid.
|
||||||
defp maybe_update_paid_filter(socket, %{"paid_filter" => filter_str}) do
|
defp maybe_update_cycle_status_filter(socket, %{"cycle_status_filter" => filter_str}) do
|
||||||
filter = determine_paid_filter(filter_str)
|
filter = determine_cycle_status_filter(filter_str)
|
||||||
assign(socket, :paid_filter, filter)
|
assign(socket, :cycle_status_filter, filter)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_update_paid_filter(socket, _params) do
|
defp maybe_update_cycle_status_filter(socket, _params) do
|
||||||
# Reset filter if not in URL params
|
# Reset filter if not in URL params
|
||||||
assign(socket, :paid_filter, nil)
|
assign(socket, :cycle_status_filter, nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Determines valid paid filter from URL parameter.
|
# Determines valid cycle status filter from URL parameter.
|
||||||
#
|
#
|
||||||
# SECURITY: This function whitelists allowed filter values. Only "paid" and "not_paid"
|
# SECURITY: This function whitelists allowed filter values. Only "paid" and "unpaid"
|
||||||
# are accepted - all other input (including malicious strings) falls back to nil.
|
# are accepted - all other input (including malicious strings) falls back to nil.
|
||||||
# This ensures no raw user input is ever passed to Ash.Query.filter/2, following
|
# This ensures no raw user input is ever passed to filter functions.
|
||||||
# Ash's security recommendation to never pass untrusted input directly to filters.
|
defp determine_cycle_status_filter("paid"), do: :paid
|
||||||
defp determine_paid_filter("paid"), do: :paid
|
defp determine_cycle_status_filter("unpaid"), do: :unpaid
|
||||||
defp determine_paid_filter("not_paid"), do: :not_paid
|
defp determine_cycle_status_filter(_), do: nil
|
||||||
defp determine_paid_filter(_), do: nil
|
|
||||||
|
|
||||||
# Updates show_current_cycle from URL parameters if present.
|
# Updates show_current_cycle from URL parameters if present.
|
||||||
defp maybe_update_show_current_cycle(socket, %{"show_current_cycle" => "true"}) do
|
defp maybe_update_show_current_cycle(socket, %{"show_current_cycle" => "true"}) do
|
||||||
|
|
@ -1122,45 +1094,6 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
socket
|
socket
|
||||||
end
|
end
|
||||||
|
|
||||||
# Updates membership fee status filter from URL parameters if present.
|
|
||||||
defp maybe_update_membership_fee_status_filter(socket, %{"membership_fee_filter" => filter_str}) do
|
|
||||||
filter = determine_membership_fee_filter(filter_str)
|
|
||||||
assign(socket, :membership_fee_status_filter, filter)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp maybe_update_membership_fee_status_filter(socket, _params) do
|
|
||||||
socket
|
|
||||||
end
|
|
||||||
|
|
||||||
# Determines valid membership fee filter from URL parameter.
|
|
||||||
#
|
|
||||||
# SECURITY: This function whitelists allowed filter values.
|
|
||||||
defp determine_membership_fee_filter("unpaid_last"), do: :unpaid_last
|
|
||||||
defp determine_membership_fee_filter("unpaid_current"), do: :unpaid_current
|
|
||||||
defp determine_membership_fee_filter(_), do: nil
|
|
||||||
|
|
||||||
# Applies membership fee status filter to members list.
|
|
||||||
defp apply_membership_fee_status_filter(members, nil, _show_current), do: members
|
|
||||||
|
|
||||||
defp apply_membership_fee_status_filter(members, :unpaid_last, _show_current) do
|
|
||||||
MembershipFeeStatus.filter_unpaid_members(members, false)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp apply_membership_fee_status_filter(members, :unpaid_current, _show_current) do
|
|
||||||
MembershipFeeStatus.filter_unpaid_members(members, true)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Adds membership fee filter to query params if set.
|
|
||||||
defp maybe_add_membership_fee_filter(params, nil), do: params
|
|
||||||
|
|
||||||
defp maybe_add_membership_fee_filter(params, :unpaid_last) do
|
|
||||||
Map.put(params, "membership_fee_filter", "unpaid_last")
|
|
||||||
end
|
|
||||||
|
|
||||||
defp maybe_add_membership_fee_filter(params, :unpaid_current) do
|
|
||||||
Map.put(params, "membership_fee_filter", "unpaid_current")
|
|
||||||
end
|
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
# Helper Functions for Custom Field Values
|
# Helper Functions for Custom Field Values
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
<.live_component
|
<.live_component
|
||||||
module={MvWeb.Components.PaymentFilterComponent}
|
module={MvWeb.Components.PaymentFilterComponent}
|
||||||
id="payment-filter"
|
id="payment-filter"
|
||||||
paid_filter={@paid_filter}
|
cycle_status_filter={@cycle_status_filter}
|
||||||
member_count={length(@members)}
|
member_count={length(@members)}
|
||||||
/>
|
/>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
|
|
@ -60,47 +60,6 @@
|
||||||
<.icon name="hero-arrow-path" class="size-4" />
|
<.icon name="hero-arrow-path" class="size-4" />
|
||||||
{if(@show_current_cycle, do: gettext("Current Cycle"), else: gettext("Last Cycle"))}
|
{if(@show_current_cycle, do: gettext("Current Cycle"), else: gettext("Last Cycle"))}
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown">
|
|
||||||
<label tabindex="0" class="btn btn-sm btn-outline">
|
|
||||||
<.icon name="hero-funnel" class="size-4" />
|
|
||||||
{gettext("Membership Fee")}
|
|
||||||
</label>
|
|
||||||
<ul
|
|
||||||
tabindex="0"
|
|
||||||
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
phx-click="filter_unpaid_cycles"
|
|
||||||
phx-value-filter="unpaid_last"
|
|
||||||
class={if(@membership_fee_status_filter == :unpaid_last, do: "active", else: "")}
|
|
||||||
>
|
|
||||||
{gettext("Unpaid in last cycle")}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
phx-click="filter_unpaid_cycles"
|
|
||||||
phx-value-filter="unpaid_current"
|
|
||||||
class={if(@membership_fee_status_filter == :unpaid_current, do: "active", else: "")}
|
|
||||||
>
|
|
||||||
{gettext("Unpaid in current cycle")}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
phx-click="filter_unpaid_cycles"
|
|
||||||
phx-value-filter=""
|
|
||||||
class={if(@membership_fee_status_filter == nil, do: "active", else: "")}
|
|
||||||
>
|
|
||||||
{gettext("All")}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<.live_component
|
<.live_component
|
||||||
module={MvWeb.Components.FieldVisibilityDropdownComponent}
|
module={MvWeb.Components.FieldVisibilityDropdownComponent}
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,41 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatus do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Filters members by cycle status (paid or unpaid).
|
||||||
|
|
||||||
|
Returns members that have the specified status in either the last completed cycle
|
||||||
|
or the current cycle, depending on `show_current`.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
- `members` - List of member structs with loaded cycles
|
||||||
|
- `status` - Cycle status to filter by (`:paid` or `:unpaid`)
|
||||||
|
- `show_current` - If true, filter by current cycle; if false, filter by last completed cycle
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
|
||||||
|
List of members with the specified cycle status
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
# Filter unpaid members in last cycle
|
||||||
|
iex> filter_members_by_cycle_status(members, :unpaid, false)
|
||||||
|
[%Member{}, ...]
|
||||||
|
|
||||||
|
# Filter paid members in current cycle
|
||||||
|
iex> filter_members_by_cycle_status(members, :paid, true)
|
||||||
|
[%Member{}, ...]
|
||||||
|
"""
|
||||||
|
@spec filter_members_by_cycle_status([Member.t()], :paid | :unpaid, boolean()) :: [Member.t()]
|
||||||
|
def filter_members_by_cycle_status(members, status, show_current \\ false)
|
||||||
|
when status in [:paid, :unpaid] do
|
||||||
|
Enum.filter(members, fn member ->
|
||||||
|
member_status = get_cycle_status_for_member(member, show_current)
|
||||||
|
member_status == status
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Filters members by unpaid cycle status.
|
Filters members by unpaid cycle status.
|
||||||
|
|
||||||
|
|
@ -127,13 +162,14 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatus do
|
||||||
## Returns
|
## Returns
|
||||||
|
|
||||||
List of members with unpaid cycles
|
List of members with unpaid cycles
|
||||||
|
|
||||||
|
## Deprecated
|
||||||
|
|
||||||
|
This function is kept for backwards compatibility. Use `filter_members_by_cycle_status/3` instead.
|
||||||
"""
|
"""
|
||||||
@spec filter_unpaid_members([Member.t()], boolean()) :: [Member.t()]
|
@spec filter_unpaid_members([Member.t()], boolean()) :: [Member.t()]
|
||||||
def filter_unpaid_members(members, show_current \\ false) do
|
def filter_unpaid_members(members, show_current \\ false) do
|
||||||
Enum.filter(members, fn member ->
|
filter_members_by_cycle_status(members, :unpaid, show_current)
|
||||||
status = get_cycle_status_for_member(member, show_current)
|
|
||||||
status == :unpaid
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Private helper function to format status label
|
# Private helper function to format status label
|
||||||
|
|
|
||||||
|
|
@ -144,11 +144,9 @@ msgstr "Notizen"
|
||||||
|
|
||||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
#: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
||||||
#: lib/mv_web/translations/member_fields.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Paid"
|
msgid "Paid"
|
||||||
msgstr "Bezahlt"
|
msgstr "Bezahlt"
|
||||||
|
|
@ -188,7 +186,6 @@ msgid "Street"
|
||||||
msgstr "Straße"
|
msgstr "Straße"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex
|
#: lib/mv_web/live/member_live/index/formatter.ex
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -201,7 +198,6 @@ msgid "Show Member"
|
||||||
msgstr "Mitglied anzeigen"
|
msgstr "Mitglied anzeigen"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex
|
#: lib/mv_web/live/member_live/index/formatter.ex
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
|
|
@ -788,7 +784,7 @@ msgid "This field cannot be empty"
|
||||||
msgstr "Dieses Feld darf nicht leer bleiben"
|
msgstr "Dieses Feld darf nicht leer bleiben"
|
||||||
|
|
||||||
#: lib/mv_web/components/core_components.ex
|
#: lib/mv_web/components/core_components.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr "Alle"
|
msgstr "Alle"
|
||||||
|
|
@ -798,11 +794,6 @@ msgstr "Alle"
|
||||||
msgid "Filter by payment status"
|
msgid "Filter by payment status"
|
||||||
msgstr "Nach Zahlungsstatus filtern"
|
msgstr "Nach Zahlungsstatus filtern"
|
||||||
|
|
||||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Not paid"
|
|
||||||
msgstr "Nicht bezahlt"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Payment filter"
|
msgid "Payment filter"
|
||||||
|
|
@ -1205,6 +1196,7 @@ msgstr "Zeitraum"
|
||||||
msgid "Total Contributions"
|
msgid "Total Contributions"
|
||||||
msgstr "Gesamtbeiträge"
|
msgstr "Gesamtbeiträge"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
#: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
|
|
@ -1570,7 +1562,6 @@ msgid "Mark as unpaid"
|
||||||
msgstr "Als unbezahlt markieren"
|
msgstr "Als unbezahlt markieren"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Membership Fee"
|
msgid "Membership Fee"
|
||||||
|
|
@ -1716,16 +1707,6 @@ msgstr "Zum letzten abgeschlossenen Zyklus wechseln"
|
||||||
msgid "Type"
|
msgid "Type"
|
||||||
msgstr "Art"
|
msgstr "Art"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Unpaid in current cycle"
|
|
||||||
msgstr "Unbezahlt im aktuellen Zyklus"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Unpaid in last cycle"
|
|
||||||
msgstr "Unbezahlt im letzten Zyklus"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Use this form to manage membership fee types in your database."
|
msgid "Use this form to manage membership fee types in your database."
|
||||||
|
|
@ -1837,6 +1818,11 @@ msgstr ""
|
||||||
msgid "You are about to delete all %{count} cycles for this member."
|
msgid "You are about to delete all %{count} cycles for this member."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "All payment statuses"
|
||||||
|
#~ msgstr "Jeder Zahlungs-Zustand"
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/custom_field_live/show.ex
|
#~ #: lib/mv_web/live/custom_field_live/show.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "Auto-generated identifier (immutable)"
|
#~ msgid "Auto-generated identifier (immutable)"
|
||||||
|
|
@ -1889,6 +1875,11 @@ msgstr ""
|
||||||
#~ msgid "New Custom field"
|
#~ msgid "New Custom field"
|
||||||
#~ msgstr "Benutzerdefiniertes Feld speichern"
|
#~ msgstr "Benutzerdefiniertes Feld speichern"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Not paid"
|
||||||
|
#~ msgstr "Nicht bezahlt"
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/user_live/form.ex
|
#~ #: lib/mv_web/live/user_live/form.ex
|
||||||
#~ #: lib/mv_web/live/user_live/show.ex
|
#~ #: lib/mv_web/live/user_live/show.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
|
@ -1916,6 +1907,16 @@ msgstr ""
|
||||||
#~ msgid "This data is for demonstration purposes only (mockup)."
|
#~ msgid "This data is for demonstration purposes only (mockup)."
|
||||||
#~ msgstr "Diese Daten dienen nur zu Demonstrationszwecken (Mockup)."
|
#~ msgstr "Diese Daten dienen nur zu Demonstrationszwecken (Mockup)."
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/member_live/index.html.heex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Unpaid in current cycle"
|
||||||
|
#~ msgstr "Unbezahlt im aktuellen Zyklus"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/member_live/index.html.heex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Unpaid in last cycle"
|
||||||
|
#~ msgstr "Unbezahlt im letzten Zyklus"
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/contribution_settings_live.ex
|
#~ #: lib/mv_web/live/contribution_settings_live.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "View Example Member"
|
#~ msgid "View Example Member"
|
||||||
|
|
|
||||||
|
|
@ -145,11 +145,9 @@ msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
#: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
||||||
#: lib/mv_web/translations/member_fields.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Paid"
|
msgid "Paid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -189,7 +187,6 @@ msgid "Street"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex
|
#: lib/mv_web/live/member_live/index/formatter.ex
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -202,7 +199,6 @@ msgid "Show Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex
|
#: lib/mv_web/live/member_live/index/formatter.ex
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
|
|
@ -789,7 +785,7 @@ msgid "This field cannot be empty"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/core_components.ex
|
#: lib/mv_web/components/core_components.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -799,11 +795,6 @@ msgstr ""
|
||||||
msgid "Filter by payment status"
|
msgid "Filter by payment status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Not paid"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Payment filter"
|
msgid "Payment filter"
|
||||||
|
|
@ -1206,6 +1197,7 @@ msgstr ""
|
||||||
msgid "Total Contributions"
|
msgid "Total Contributions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
#: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
|
|
@ -1571,7 +1563,6 @@ msgid "Mark as unpaid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Membership Fee"
|
msgid "Membership Fee"
|
||||||
|
|
@ -1717,16 +1708,6 @@ msgstr ""
|
||||||
msgid "Type"
|
msgid "Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Unpaid in current cycle"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Unpaid in last cycle"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Use this form to manage membership fee types in your database."
|
msgid "Use this form to manage membership fee types in your database."
|
||||||
|
|
|
||||||
|
|
@ -145,11 +145,9 @@ msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
#: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
||||||
#: lib/mv_web/translations/member_fields.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Paid"
|
msgid "Paid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -189,7 +187,6 @@ msgid "Street"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex
|
#: lib/mv_web/live/member_live/index/formatter.ex
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -202,7 +199,6 @@ msgid "Show Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex
|
#: lib/mv_web/live/member_live/index/formatter.ex
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
|
|
@ -789,7 +785,7 @@ msgid "This field cannot be empty"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/core_components.ex
|
#: lib/mv_web/components/core_components.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -799,11 +795,6 @@ msgstr ""
|
||||||
msgid "Filter by payment status"
|
msgid "Filter by payment status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Not paid"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Payment filter"
|
msgid "Payment filter"
|
||||||
|
|
@ -1206,6 +1197,7 @@ msgstr ""
|
||||||
msgid "Total Contributions"
|
msgid "Total Contributions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
#: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
|
|
@ -1571,7 +1563,6 @@ msgid "Mark as unpaid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Membership Fee"
|
msgid "Membership Fee"
|
||||||
|
|
@ -1717,16 +1708,6 @@ msgstr ""
|
||||||
msgid "Type"
|
msgid "Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Unpaid in current cycle"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Unpaid in last cycle"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Use this form to manage membership fee types in your database."
|
msgid "Use this form to manage membership fee types in your database."
|
||||||
|
|
@ -1838,6 +1819,11 @@ msgstr ""
|
||||||
msgid "You are about to delete all %{count} cycles for this member."
|
msgid "You are about to delete all %{count} cycles for this member."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "All payment statuses"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/custom_field_live/show.ex
|
#~ #: lib/mv_web/live/custom_field_live/show.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "Auto-generated identifier (immutable)"
|
#~ msgid "Auto-generated identifier (immutable)"
|
||||||
|
|
@ -1912,6 +1898,11 @@ msgstr ""
|
||||||
#~ msgid "New Custom field"
|
#~ msgid "New Custom field"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Not paid"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/user_live/show.ex
|
#~ #: lib/mv_web/live/user_live/show.ex
|
||||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
#~ msgid "Not set"
|
#~ msgid "Not set"
|
||||||
|
|
@ -1938,6 +1929,16 @@ msgstr ""
|
||||||
#~ msgid "This data is for demonstration purposes only (mockup)."
|
#~ msgid "This data is for demonstration purposes only (mockup)."
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/member_live/index.html.heex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Unpaid in current cycle"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/member_live/index.html.heex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Unpaid in last cycle"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/contribution_settings_live.ex
|
#~ #: lib/mv_web/live/contribution_settings_live.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "View Example Member"
|
#~ msgid "View Example Member"
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ defmodule MvWeb.Components.PaymentFilterComponentTest do
|
||||||
Unit tests for the PaymentFilterComponent.
|
Unit tests for the PaymentFilterComponent.
|
||||||
|
|
||||||
Tests cover:
|
Tests cover:
|
||||||
- Rendering in all 3 filter states (nil, :paid, :not_paid)
|
- Rendering in all 3 filter states (nil, :paid, :unpaid)
|
||||||
- Event emission when selecting options
|
- Event emission when selecting options
|
||||||
- ARIA attributes for accessibility
|
- ARIA attributes for accessibility
|
||||||
- Dropdown open/close behavior
|
- Dropdown open/close behavior
|
||||||
|
|
@ -25,15 +25,15 @@ defmodule MvWeb.Components.PaymentFilterComponentTest do
|
||||||
|
|
||||||
test "renders with paid filter active", %{conn: conn} do
|
test "renders with paid filter active", %{conn: conn} do
|
||||||
conn = conn_with_oidc_user(conn)
|
conn = conn_with_oidc_user(conn)
|
||||||
{:ok, view, _html} = live(conn, "/members?paid_filter=paid")
|
{:ok, view, _html} = live(conn, "/members?cycle_status_filter=paid")
|
||||||
|
|
||||||
# Should show badge when filter is active
|
# Should show badge when filter is active
|
||||||
assert has_element?(view, "#payment-filter .badge")
|
assert has_element?(view, "#payment-filter .badge")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "renders with not_paid filter active", %{conn: conn} do
|
test "renders with unpaid filter active", %{conn: conn} do
|
||||||
conn = conn_with_oidc_user(conn)
|
conn = conn_with_oidc_user(conn)
|
||||||
{:ok, view, _html} = live(conn, "/members?paid_filter=not_paid")
|
{:ok, view, _html} = live(conn, "/members?cycle_status_filter=unpaid")
|
||||||
|
|
||||||
# Should show badge when filter is active
|
# Should show badge when filter is active
|
||||||
assert has_element?(view, "#payment-filter .badge")
|
assert has_element?(view, "#payment-filter .badge")
|
||||||
|
|
@ -82,7 +82,7 @@ defmodule MvWeb.Components.PaymentFilterComponentTest do
|
||||||
describe "filter selection" do
|
describe "filter selection" do
|
||||||
test "selecting 'All' clears the filter and updates URL", %{conn: conn} do
|
test "selecting 'All' clears the filter and updates URL", %{conn: conn} do
|
||||||
conn = conn_with_oidc_user(conn)
|
conn = conn_with_oidc_user(conn)
|
||||||
{:ok, view, _html} = live(conn, "/members?paid_filter=paid")
|
{:ok, view, _html} = live(conn, "/members?cycle_status_filter=paid")
|
||||||
|
|
||||||
# Open dropdown
|
# Open dropdown
|
||||||
view
|
view
|
||||||
|
|
@ -94,7 +94,7 @@ defmodule MvWeb.Components.PaymentFilterComponentTest do
|
||||||
|> element("#payment-filter button[phx-value-filter='']")
|
|> element("#payment-filter button[phx-value-filter='']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# URL should not contain paid_filter param - wait for patch
|
# URL should not contain cycle_status_filter param - wait for patch
|
||||||
assert_patch(view)
|
assert_patch(view)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -112,12 +112,12 @@ defmodule MvWeb.Components.PaymentFilterComponentTest do
|
||||||
|> element("#payment-filter button[phx-value-filter='paid']")
|
|> element("#payment-filter button[phx-value-filter='paid']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# Wait for patch and check URL contains paid_filter=paid
|
# Wait for patch and check URL contains cycle_status_filter=paid
|
||||||
path = assert_patch(view)
|
path = assert_patch(view)
|
||||||
assert path =~ "paid_filter=paid"
|
assert path =~ "cycle_status_filter=paid"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "selecting 'Not paid' sets the filter and updates URL", %{conn: conn} do
|
test "selecting 'Unpaid' sets the filter and updates URL", %{conn: conn} do
|
||||||
conn = conn_with_oidc_user(conn)
|
conn = conn_with_oidc_user(conn)
|
||||||
{:ok, view, _html} = live(conn, "/members")
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
|
@ -126,14 +126,14 @@ defmodule MvWeb.Components.PaymentFilterComponentTest do
|
||||||
|> element("#payment-filter button[aria-haspopup='true']")
|
|> element("#payment-filter button[aria-haspopup='true']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# Select "Not paid" option
|
# Select "Unpaid" option
|
||||||
view
|
view
|
||||||
|> element("#payment-filter button[phx-value-filter='not_paid']")
|
|> element("#payment-filter button[phx-value-filter='unpaid']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# Wait for patch and check URL contains paid_filter=not_paid
|
# Wait for patch and check URL contains cycle_status_filter=unpaid
|
||||||
path = assert_patch(view)
|
path = assert_patch(view)
|
||||||
assert path =~ "paid_filter=not_paid"
|
assert path =~ "cycle_status_filter=unpaid"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -166,7 +166,7 @@ defmodule MvWeb.Components.PaymentFilterComponentTest do
|
||||||
|
|
||||||
test "has aria-checked on selected option", %{conn: conn} do
|
test "has aria-checked on selected option", %{conn: conn} do
|
||||||
conn = conn_with_oidc_user(conn)
|
conn = conn_with_oidc_user(conn)
|
||||||
{:ok, view, _html} = live(conn, "/members?paid_filter=paid")
|
{:ok, view, _html} = live(conn, "/members?cycle_status_filter=paid")
|
||||||
|
|
||||||
# Open dropdown
|
# Open dropdown
|
||||||
view
|
view
|
||||||
|
|
|
||||||
|
|
@ -235,4 +235,134 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
|
||||||
assert result == nil
|
assert result == nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "filter_members_by_cycle_status/3" do
|
||||||
|
test "filters paid members in last cycle" do
|
||||||
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
|
today = Date.utc_today()
|
||||||
|
last_year_start = Date.new!(today.year - 1, 1, 1)
|
||||||
|
|
||||||
|
# Member with paid last cycle
|
||||||
|
member1 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
|
create_cycle(member1, fee_type, %{cycle_start: last_year_start, status: :paid})
|
||||||
|
|
||||||
|
# Member with unpaid last cycle
|
||||||
|
member2 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
|
create_cycle(member2, fee_type, %{cycle_start: last_year_start, status: :unpaid})
|
||||||
|
|
||||||
|
members =
|
||||||
|
[member1, member2]
|
||||||
|
|> Enum.map(fn m ->
|
||||||
|
m
|
||||||
|
|> Ash.load!(membership_fee_cycles: [:membership_fee_type])
|
||||||
|
|> Ash.load!(:membership_fee_type)
|
||||||
|
end)
|
||||||
|
|
||||||
|
filtered = MembershipFeeStatus.filter_members_by_cycle_status(members, :paid, false)
|
||||||
|
|
||||||
|
assert length(filtered) == 1
|
||||||
|
assert List.first(filtered).id == member1.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filters unpaid members in last cycle" do
|
||||||
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
|
today = Date.utc_today()
|
||||||
|
last_year_start = Date.new!(today.year - 1, 1, 1)
|
||||||
|
|
||||||
|
# Member with paid last cycle
|
||||||
|
member1 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
|
create_cycle(member1, fee_type, %{cycle_start: last_year_start, status: :paid})
|
||||||
|
|
||||||
|
# Member with unpaid last cycle
|
||||||
|
member2 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
|
create_cycle(member2, fee_type, %{cycle_start: last_year_start, status: :unpaid})
|
||||||
|
|
||||||
|
members =
|
||||||
|
[member1, member2]
|
||||||
|
|> Enum.map(fn m ->
|
||||||
|
m
|
||||||
|
|> Ash.load!(membership_fee_cycles: [:membership_fee_type])
|
||||||
|
|> Ash.load!(:membership_fee_type)
|
||||||
|
end)
|
||||||
|
|
||||||
|
filtered = MembershipFeeStatus.filter_members_by_cycle_status(members, :unpaid, false)
|
||||||
|
|
||||||
|
assert length(filtered) == 1
|
||||||
|
assert List.first(filtered).id == member2.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filters paid members in current cycle" do
|
||||||
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
|
today = Date.utc_today()
|
||||||
|
current_year_start = Date.new!(today.year, 1, 1)
|
||||||
|
|
||||||
|
# Member with paid current cycle
|
||||||
|
member1 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
|
create_cycle(member1, fee_type, %{cycle_start: current_year_start, status: :paid})
|
||||||
|
|
||||||
|
# Member with unpaid current cycle
|
||||||
|
member2 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
|
create_cycle(member2, fee_type, %{cycle_start: current_year_start, status: :unpaid})
|
||||||
|
|
||||||
|
members =
|
||||||
|
[member1, member2]
|
||||||
|
|> Enum.map(fn m ->
|
||||||
|
m
|
||||||
|
|> Ash.load!(membership_fee_cycles: [:membership_fee_type])
|
||||||
|
|> Ash.load!(:membership_fee_type)
|
||||||
|
end)
|
||||||
|
|
||||||
|
filtered = MembershipFeeStatus.filter_members_by_cycle_status(members, :paid, true)
|
||||||
|
|
||||||
|
assert length(filtered) == 1
|
||||||
|
assert List.first(filtered).id == member1.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filters unpaid members in current cycle" do
|
||||||
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
|
today = Date.utc_today()
|
||||||
|
current_year_start = Date.new!(today.year, 1, 1)
|
||||||
|
|
||||||
|
# Member with paid current cycle
|
||||||
|
member1 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
|
create_cycle(member1, fee_type, %{cycle_start: current_year_start, status: :paid})
|
||||||
|
|
||||||
|
# Member with unpaid current cycle
|
||||||
|
member2 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
|
create_cycle(member2, fee_type, %{cycle_start: current_year_start, status: :unpaid})
|
||||||
|
|
||||||
|
members =
|
||||||
|
[member1, member2]
|
||||||
|
|> Enum.map(fn m ->
|
||||||
|
m
|
||||||
|
|> Ash.load!(membership_fee_cycles: [:membership_fee_type])
|
||||||
|
|> Ash.load!(:membership_fee_type)
|
||||||
|
end)
|
||||||
|
|
||||||
|
filtered = MembershipFeeStatus.filter_members_by_cycle_status(members, :unpaid, true)
|
||||||
|
|
||||||
|
assert length(filtered) == 1
|
||||||
|
assert List.first(filtered).id == member2.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns all members when filter is nil" do
|
||||||
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
|
member1 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
|
member2 = create_member(%{membership_fee_type_id: fee_type.id})
|
||||||
|
|
||||||
|
members =
|
||||||
|
[member1, member2]
|
||||||
|
|> Enum.map(fn m ->
|
||||||
|
m
|
||||||
|
|> Ash.load!(membership_fee_cycles: [:membership_fee_type])
|
||||||
|
|> Ash.load!(:membership_fee_type)
|
||||||
|
end)
|
||||||
|
|
||||||
|
# filter_unpaid_members should still work for backwards compatibility
|
||||||
|
filtered = MembershipFeeStatus.filter_unpaid_members(members, false)
|
||||||
|
|
||||||
|
# Both members have no cycles, so both should be filtered out
|
||||||
|
assert length(filtered) == 0
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -456,4 +456,205 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
assert has_element?(view, "#flash-group")
|
assert has_element?(view, "#flash-group")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "cycle status filter" do
|
||||||
|
alias Mv.MembershipFees.MembershipFeeType
|
||||||
|
alias Mv.MembershipFees.MembershipFeeCycle
|
||||||
|
|
||||||
|
# Helper to create a membership fee type
|
||||||
|
defp create_fee_type(attrs) do
|
||||||
|
default_attrs = %{
|
||||||
|
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
||||||
|
amount: Decimal.new("50.00"),
|
||||||
|
interval: :yearly
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs = Map.merge(default_attrs, attrs)
|
||||||
|
|
||||||
|
MembershipFeeType
|
||||||
|
|> Ash.Changeset.for_create(:create, attrs)
|
||||||
|
|> Ash.create!()
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to create a member
|
||||||
|
defp create_member(attrs) do
|
||||||
|
default_attrs = %{
|
||||||
|
first_name: "Test",
|
||||||
|
last_name: "Member",
|
||||||
|
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs = Map.merge(default_attrs, attrs)
|
||||||
|
|
||||||
|
Mv.Membership.Member
|
||||||
|
|> Ash.Changeset.for_create(:create_member, attrs)
|
||||||
|
|> Ash.create!()
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to create a cycle
|
||||||
|
defp create_cycle(member, fee_type, attrs) do
|
||||||
|
# Delete any auto-generated cycles first to avoid conflicts
|
||||||
|
existing_cycles =
|
||||||
|
MembershipFeeCycle
|
||||||
|
|> Ash.Query.filter(member_id == ^member.id)
|
||||||
|
|> Ash.read!()
|
||||||
|
|
||||||
|
Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle) end)
|
||||||
|
|
||||||
|
default_attrs = %{
|
||||||
|
cycle_start: ~D[2023-01-01],
|
||||||
|
amount: Decimal.new("50.00"),
|
||||||
|
member_id: member.id,
|
||||||
|
membership_fee_type_id: fee_type.id,
|
||||||
|
status: :unpaid
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs = Map.merge(default_attrs, attrs)
|
||||||
|
|
||||||
|
MembershipFeeCycle
|
||||||
|
|> Ash.Changeset.for_create(:create, attrs)
|
||||||
|
|> Ash.create!()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filter shows only members with paid status in last cycle", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
|
today = Date.utc_today()
|
||||||
|
last_year_start = Date.new!(today.year - 1, 1, 1)
|
||||||
|
|
||||||
|
# Member with paid last cycle
|
||||||
|
paid_member =
|
||||||
|
create_member(%{
|
||||||
|
first_name: "PaidLast",
|
||||||
|
membership_fee_type_id: fee_type.id
|
||||||
|
})
|
||||||
|
|
||||||
|
create_cycle(paid_member, fee_type, %{cycle_start: last_year_start, status: :paid})
|
||||||
|
|
||||||
|
# Member with unpaid last cycle
|
||||||
|
unpaid_member =
|
||||||
|
create_member(%{
|
||||||
|
first_name: "UnpaidLast",
|
||||||
|
membership_fee_type_id: fee_type.id
|
||||||
|
})
|
||||||
|
|
||||||
|
create_cycle(unpaid_member, fee_type, %{cycle_start: last_year_start, status: :unpaid})
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, "/members?cycle_status_filter=paid")
|
||||||
|
|
||||||
|
assert html =~ "PaidLast"
|
||||||
|
refute html =~ "UnpaidLast"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filter shows only members with unpaid status in last cycle", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
|
today = Date.utc_today()
|
||||||
|
last_year_start = Date.new!(today.year - 1, 1, 1)
|
||||||
|
|
||||||
|
# Member with paid last cycle
|
||||||
|
paid_member =
|
||||||
|
create_member(%{
|
||||||
|
first_name: "PaidLast",
|
||||||
|
membership_fee_type_id: fee_type.id
|
||||||
|
})
|
||||||
|
|
||||||
|
create_cycle(paid_member, fee_type, %{cycle_start: last_year_start, status: :paid})
|
||||||
|
|
||||||
|
# Member with unpaid last cycle
|
||||||
|
unpaid_member =
|
||||||
|
create_member(%{
|
||||||
|
first_name: "UnpaidLast",
|
||||||
|
membership_fee_type_id: fee_type.id
|
||||||
|
})
|
||||||
|
|
||||||
|
create_cycle(unpaid_member, fee_type, %{cycle_start: last_year_start, status: :unpaid})
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, "/members?cycle_status_filter=unpaid")
|
||||||
|
|
||||||
|
refute html =~ "PaidLast"
|
||||||
|
assert html =~ "UnpaidLast"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filter shows only members with paid status in current cycle", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
|
today = Date.utc_today()
|
||||||
|
current_year_start = Date.new!(today.year, 1, 1)
|
||||||
|
|
||||||
|
# Member with paid current cycle
|
||||||
|
paid_member =
|
||||||
|
create_member(%{
|
||||||
|
first_name: "PaidCurrent",
|
||||||
|
membership_fee_type_id: fee_type.id
|
||||||
|
})
|
||||||
|
|
||||||
|
create_cycle(paid_member, fee_type, %{cycle_start: current_year_start, status: :paid})
|
||||||
|
|
||||||
|
# Member with unpaid current cycle
|
||||||
|
unpaid_member =
|
||||||
|
create_member(%{
|
||||||
|
first_name: "UnpaidCurrent",
|
||||||
|
membership_fee_type_id: fee_type.id
|
||||||
|
})
|
||||||
|
|
||||||
|
create_cycle(unpaid_member, fee_type, %{cycle_start: current_year_start, status: :unpaid})
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, "/members?cycle_status_filter=paid&show_current_cycle=true")
|
||||||
|
|
||||||
|
assert html =~ "PaidCurrent"
|
||||||
|
refute html =~ "UnpaidCurrent"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filter shows only members with unpaid status in current cycle", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
fee_type = create_fee_type(%{interval: :yearly})
|
||||||
|
today = Date.utc_today()
|
||||||
|
current_year_start = Date.new!(today.year, 1, 1)
|
||||||
|
|
||||||
|
# Member with paid current cycle
|
||||||
|
paid_member =
|
||||||
|
create_member(%{
|
||||||
|
first_name: "PaidCurrent",
|
||||||
|
membership_fee_type_id: fee_type.id
|
||||||
|
})
|
||||||
|
|
||||||
|
create_cycle(paid_member, fee_type, %{cycle_start: current_year_start, status: :paid})
|
||||||
|
|
||||||
|
# Member with unpaid current cycle
|
||||||
|
unpaid_member =
|
||||||
|
create_member(%{
|
||||||
|
first_name: "UnpaidCurrent",
|
||||||
|
membership_fee_type_id: fee_type.id
|
||||||
|
})
|
||||||
|
|
||||||
|
create_cycle(unpaid_member, fee_type, %{cycle_start: current_year_start, status: :unpaid})
|
||||||
|
|
||||||
|
{:ok, _view, html} =
|
||||||
|
live(conn, "/members?cycle_status_filter=unpaid&show_current_cycle=true")
|
||||||
|
|
||||||
|
refute html =~ "PaidCurrent"
|
||||||
|
assert html =~ "UnpaidCurrent"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "toggle cycle view updates URL and preserves filter", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
|
||||||
|
# Start with last cycle view and paid filter
|
||||||
|
{:ok, view, _html} = live(conn, "/members?cycle_status_filter=paid")
|
||||||
|
|
||||||
|
# Toggle to current cycle - this should update URL and preserve filter
|
||||||
|
# Use the button in the membership fee status column header
|
||||||
|
view
|
||||||
|
|> element("button[phx-click='toggle_cycle_view'].btn-xs")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Wait for patch to complete
|
||||||
|
path = assert_patch(view)
|
||||||
|
|
||||||
|
# URL should contain both filter and show_current_cycle
|
||||||
|
assert path =~ "cycle_status_filter=paid"
|
||||||
|
assert path =~ "show_current_cycle=true"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue