feat: add new filter component to members view
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
1011b94acf
commit
f996aee6b2
9 changed files with 891 additions and 656 deletions
454
lib/mv_web/live/components/member_filter_component.ex
Normal file
454
lib/mv_web/live/components/member_filter_component.ex
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
defmodule MvWeb.Components.MemberFilterComponent do
|
||||
@moduledoc """
|
||||
Provides the MemberFilter Live-Component.
|
||||
|
||||
A DaisyUI dropdown filter for filtering members by payment status and boolean custom fields.
|
||||
Uses radio inputs in a segmented control pattern (join + btn) for tri-state boolean filters.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
- Uses `div` panel instead of `ul.menu/li` structure to avoid DaisyUI menu styles
|
||||
(padding, display, hover, font sizes) that would interfere with form controls.
|
||||
- Filter controls are form elements (fieldset, radio inputs), not menu items.
|
||||
- Dropdown stays open when clicking filter segments to allow multiple filter changes.
|
||||
- Uses `phx-change` on form for radio inputs instead of individual `phx-click` events.
|
||||
|
||||
## Props
|
||||
- `:cycle_status_filter` - Current payment filter state: `nil` (all), `:paid`, or `:unpaid`
|
||||
- `:boolean_custom_fields` - List of boolean custom fields to display
|
||||
- `:boolean_filters` - Map of active boolean filters: `%{custom_field_id => true | false}`
|
||||
- `:id` - Component ID (required)
|
||||
- `:member_count` - Number of filtered members to display in badge (optional, default: 0)
|
||||
|
||||
## Events
|
||||
- Sends `{:payment_filter_changed, filter}` to parent when payment filter changes
|
||||
- Sends `{:boolean_filter_changed, custom_field_id, filter_value}` to parent when boolean filter changes
|
||||
"""
|
||||
use MvWeb, :live_component
|
||||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
{:ok, assign(socket, :open, false)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
socket =
|
||||
socket
|
||||
|> assign(:id, assigns.id)
|
||||
|> assign(:cycle_status_filter, assigns[:cycle_status_filter])
|
||||
|> assign(:boolean_custom_fields, assigns[:boolean_custom_fields] || [])
|
||||
|> assign(:boolean_filters, assigns[:boolean_filters] || %{})
|
||||
|> assign(:member_count, assigns[:member_count] || 0)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
class="relative"
|
||||
id={@id}
|
||||
phx-window-keydown={@open && "close_dropdown"}
|
||||
phx-key="Escape"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
tabindex="0"
|
||||
class={[
|
||||
"btn gap-2",
|
||||
(@cycle_status_filter || active_boolean_filters_count(@boolean_filters) > 0) && "btn-active"
|
||||
]}
|
||||
phx-click="toggle_dropdown"
|
||||
phx-target={@myself}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={to_string(@open)}
|
||||
aria-label={gettext("Filter members")}
|
||||
>
|
||||
<.icon name="hero-funnel" class="h-5 w-5" />
|
||||
<span class="hidden sm:inline">
|
||||
{button_label(@cycle_status_filter, @boolean_custom_fields, @boolean_filters)}
|
||||
</span>
|
||||
<span
|
||||
:if={active_boolean_filters_count(@boolean_filters) > 0}
|
||||
class="badge badge-primary badge-sm"
|
||||
>
|
||||
{active_boolean_filters_count(@boolean_filters)}
|
||||
</span>
|
||||
<span
|
||||
:if={@cycle_status_filter && active_boolean_filters_count(@boolean_filters) == 0}
|
||||
class="badge badge-primary badge-sm"
|
||||
>
|
||||
{@member_count}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!--
|
||||
NOTE: We use a div panel instead of ul.menu/li structure to avoid DaisyUI menu styles
|
||||
(padding, display, hover, font sizes) that would interfere with our form controls.
|
||||
Filter controls are form elements (fieldset, radio inputs), not menu items.
|
||||
We use relative/absolute positioning instead of DaisyUI dropdown classes to have
|
||||
full control over the open/close state via LiveView.
|
||||
-->
|
||||
<div
|
||||
:if={@open}
|
||||
tabindex="0"
|
||||
class="absolute left-0 mt-2 w-[28rem] rounded-box border border-base-300 bg-base-100 p-4 shadow-xl z-[100]"
|
||||
phx-click-away="close_dropdown"
|
||||
phx-target={@myself}
|
||||
role="dialog"
|
||||
aria-label={gettext("Member filter")}
|
||||
>
|
||||
<form phx-change="update_filters" phx-target={@myself}>
|
||||
<!-- Payment Filter Group -->
|
||||
<div class="mb-4">
|
||||
<div class="text-xs font-semibold opacity-70 mb-2 uppercase tracking-wider">
|
||||
{gettext("Payments")}
|
||||
</div>
|
||||
<div class="grid grid-cols-[1fr_auto] items-center gap-3 py-2">
|
||||
<label class="text-sm font-medium" for="payment-filter">
|
||||
{gettext("Payment Status")}
|
||||
</label>
|
||||
<fieldset class="join" aria-label={gettext("Payment status filter")}>
|
||||
<label
|
||||
class={"#{payment_filter_label_class(@cycle_status_filter, nil)} has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary"}
|
||||
for="payment-filter-all"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id="payment-filter-all"
|
||||
name="payment_filter"
|
||||
value="all"
|
||||
class="absolute opacity-0 w-0 h-0 pointer-events-none"
|
||||
checked={@cycle_status_filter == nil}
|
||||
/>
|
||||
<span class="text-xs">{gettext("All")}</span>
|
||||
</label>
|
||||
<label
|
||||
class={"#{payment_filter_label_class(@cycle_status_filter, :paid)} has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary"}
|
||||
for="payment-filter-paid"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id="payment-filter-paid"
|
||||
name="payment_filter"
|
||||
value="paid"
|
||||
class="absolute opacity-0 w-0 h-0 pointer-events-none"
|
||||
checked={@cycle_status_filter == :paid}
|
||||
/>
|
||||
<.icon name="hero-check-circle" class="h-5 w-5" />
|
||||
<span class="text-xs">{gettext("Paid")}</span>
|
||||
</label>
|
||||
<label
|
||||
class={"#{payment_filter_label_class(@cycle_status_filter, :unpaid)} has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary"}
|
||||
for="payment-filter-unpaid"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id="payment-filter-unpaid"
|
||||
name="payment_filter"
|
||||
value="unpaid"
|
||||
class="absolute opacity-0 w-0 h-0 pointer-events-none"
|
||||
checked={@cycle_status_filter == :unpaid}
|
||||
/>
|
||||
<.icon name="hero-x-circle" class="h-5 w-5" />
|
||||
<span class="text-xs">{gettext("Unpaid")}</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Fields Group -->
|
||||
<div :if={length(@boolean_custom_fields) > 0} class="mb-2">
|
||||
<div class="text-xs font-semibold opacity-70 mb-2 uppercase tracking-wider">
|
||||
{gettext("Custom Fields")}
|
||||
</div>
|
||||
<div class="max-h-60 overflow-y-auto pr-2">
|
||||
<div
|
||||
:for={custom_field <- @boolean_custom_fields}
|
||||
class="grid grid-cols-[1fr_auto] items-center gap-3 py-2 border-b border-base-200 last:border-0"
|
||||
>
|
||||
<label
|
||||
class="text-sm font-medium"
|
||||
for={"custom-boolean-filter-#{custom_field.id}"}
|
||||
>
|
||||
{custom_field.name}
|
||||
</label>
|
||||
<fieldset
|
||||
class="join"
|
||||
aria-label={gettext("Filter by %{name}", name: custom_field.name)}
|
||||
>
|
||||
<label
|
||||
class={"#{boolean_filter_label_class(@boolean_filters, custom_field.id, nil)} has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary"}
|
||||
for={"custom-boolean-filter-#{custom_field.id}-all"}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id={"custom-boolean-filter-#{custom_field.id}-all"}
|
||||
name={"custom_boolean[#{custom_field.id}]"}
|
||||
value="all"
|
||||
class="absolute opacity-0 w-0 h-0 pointer-events-none"
|
||||
checked={Map.get(@boolean_filters, to_string(custom_field.id)) == nil}
|
||||
/>
|
||||
<span class="text-xs">{gettext("All")}</span>
|
||||
</label>
|
||||
<label
|
||||
class={"#{boolean_filter_label_class(@boolean_filters, custom_field.id, true)} has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary"}
|
||||
for={"custom-boolean-filter-#{custom_field.id}-true"}
|
||||
aria-label={gettext("Yes")}
|
||||
title={gettext("Yes")}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id={"custom-boolean-filter-#{custom_field.id}-true"}
|
||||
name={"custom_boolean[#{custom_field.id}]"}
|
||||
value="true"
|
||||
class="absolute opacity-0 w-0 h-0 pointer-events-none"
|
||||
checked={Map.get(@boolean_filters, to_string(custom_field.id)) == true}
|
||||
/>
|
||||
<.icon name="hero-check-circle" class="h-5 w-5" />
|
||||
<span class="text-xs">{gettext("Yes")}</span>
|
||||
</label>
|
||||
<label
|
||||
class={"#{boolean_filter_label_class(@boolean_filters, custom_field.id, false)} has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary"}
|
||||
for={"custom-boolean-filter-#{custom_field.id}-false"}
|
||||
aria-label={gettext("No")}
|
||||
title={gettext("No")}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id={"custom-boolean-filter-#{custom_field.id}-false"}
|
||||
name={"custom_boolean[#{custom_field.id}]"}
|
||||
value="false"
|
||||
class="absolute opacity-0 w-0 h-0 pointer-events-none"
|
||||
checked={Map.get(@boolean_filters, to_string(custom_field.id)) == false}
|
||||
/>
|
||||
<.icon name="hero-x-circle" class="h-5 w-5" />
|
||||
<span class="text-xs">{gettext("No")}</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="mt-4 flex justify-between pt-3 border-t border-base-200">
|
||||
<button
|
||||
type="button"
|
||||
phx-click="reset_filters"
|
||||
phx-target={@myself}
|
||||
class="btn btn-sm"
|
||||
>
|
||||
{gettext("Reset")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="close_dropdown"
|
||||
phx-target={@myself}
|
||||
class="btn btn-primary btn-sm"
|
||||
>
|
||||
{gettext("Close")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("toggle_dropdown", _params, socket) do
|
||||
{:noreply, assign(socket, :open, !socket.assigns.open)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("close_dropdown", _params, socket) do
|
||||
{:noreply, assign(socket, :open, false)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("update_filters", params, socket) do
|
||||
# Parse payment filter
|
||||
payment_filter =
|
||||
case Map.get(params, "payment_filter") do
|
||||
"paid" -> :paid
|
||||
"unpaid" -> :unpaid
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
# Parse boolean custom field filters (including nil values for "all")
|
||||
custom_boolean_filters_parsed =
|
||||
params
|
||||
|> Map.get("custom_boolean", %{})
|
||||
|> Enum.reduce(%{}, fn {custom_field_id_str, value_str}, acc ->
|
||||
filter_value = parse_tri_state(value_str)
|
||||
Map.put(acc, custom_field_id_str, filter_value)
|
||||
end)
|
||||
|
||||
# Update payment filter if changed
|
||||
if payment_filter != socket.assigns.cycle_status_filter do
|
||||
send(self(), {:payment_filter_changed, payment_filter})
|
||||
end
|
||||
|
||||
# Update boolean filters - send events for each changed filter
|
||||
current_filters = socket.assigns.boolean_filters
|
||||
|
||||
# Process all custom field filters from form (including those set to "all"/nil)
|
||||
# Radio buttons in a group always send a value, so all active filters are in the form
|
||||
Enum.each(custom_boolean_filters_parsed, fn {custom_field_id_str, new_value} ->
|
||||
current_value = Map.get(current_filters, custom_field_id_str)
|
||||
|
||||
# Only send event if value actually changed
|
||||
if current_value != new_value do
|
||||
send(self(), {:boolean_filter_changed, custom_field_id_str, new_value})
|
||||
end
|
||||
end)
|
||||
|
||||
# Don't close dropdown - allow multiple filter changes
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("reset_filters", _params, socket) do
|
||||
# Reset payment filter
|
||||
if socket.assigns.cycle_status_filter != nil do
|
||||
send(self(), {:payment_filter_changed, nil})
|
||||
end
|
||||
|
||||
# Reset all boolean filters
|
||||
Enum.each(socket.assigns.boolean_filters, fn {custom_field_id_str, _value} ->
|
||||
send(self(), {:boolean_filter_changed, custom_field_id_str, nil})
|
||||
end)
|
||||
|
||||
# Close dropdown after reset
|
||||
{:noreply, assign(socket, :open, false)}
|
||||
end
|
||||
|
||||
# Parse tri-state filter value: "all" | "true" | "false" -> nil | true | false
|
||||
defp parse_tri_state("true"), do: true
|
||||
defp parse_tri_state("false"), do: false
|
||||
defp parse_tri_state("all"), do: nil
|
||||
defp parse_tri_state(_), do: nil
|
||||
|
||||
# Get display label for button
|
||||
defp button_label(cycle_status_filter, boolean_custom_fields, boolean_filters) do
|
||||
# If payment filter is active, show payment filter label
|
||||
if cycle_status_filter do
|
||||
payment_filter_label(cycle_status_filter)
|
||||
else
|
||||
# Otherwise show boolean filter labels
|
||||
boolean_filter_label(boolean_custom_fields, boolean_filters)
|
||||
end
|
||||
end
|
||||
|
||||
# Get payment filter label
|
||||
defp payment_filter_label(nil), do: gettext("All")
|
||||
defp payment_filter_label(:paid), do: gettext("Paid")
|
||||
defp payment_filter_label(:unpaid), do: gettext("Unpaid")
|
||||
|
||||
# Get boolean filter label (comma-separated list of active filter names)
|
||||
defp boolean_filter_label(_boolean_custom_fields, boolean_filters)
|
||||
when map_size(boolean_filters) == 0 do
|
||||
gettext("All")
|
||||
end
|
||||
|
||||
defp boolean_filter_label(boolean_custom_fields, boolean_filters) do
|
||||
# Get names of active boolean filters
|
||||
active_filter_names =
|
||||
boolean_filters
|
||||
|> Enum.map(fn {custom_field_id_str, _value} ->
|
||||
Enum.find(boolean_custom_fields, fn cf -> to_string(cf.id) == custom_field_id_str end)
|
||||
end)
|
||||
|> Enum.filter(&(&1 != nil))
|
||||
|> Enum.map(& &1.name)
|
||||
|
||||
# Join with comma and truncate if too long
|
||||
label = Enum.join(active_filter_names, ", ")
|
||||
truncate_label(label, 30)
|
||||
end
|
||||
|
||||
# Truncate label if longer than max_length
|
||||
defp truncate_label(label, max_length) when byte_size(label) <= max_length, do: label
|
||||
|
||||
defp truncate_label(label, max_length) do
|
||||
String.slice(label, 0, max_length) <> "..."
|
||||
end
|
||||
|
||||
# Count active boolean filters
|
||||
defp active_boolean_filters_count(boolean_filters) do
|
||||
map_size(boolean_filters)
|
||||
end
|
||||
|
||||
# Get CSS classes for payment filter label based on current state
|
||||
defp payment_filter_label_class(current_filter, expected_value) do
|
||||
base_classes = "join-item btn btn-sm"
|
||||
is_active = current_filter == expected_value
|
||||
|
||||
cond do
|
||||
# All button (nil expected)
|
||||
expected_value == nil ->
|
||||
if is_active do
|
||||
"#{base_classes} btn-active"
|
||||
else
|
||||
"#{base_classes} btn"
|
||||
end
|
||||
|
||||
# Paid button
|
||||
expected_value == :paid ->
|
||||
if is_active do
|
||||
"#{base_classes} btn-success btn-active"
|
||||
else
|
||||
"#{base_classes} btn"
|
||||
end
|
||||
|
||||
# Unpaid button
|
||||
expected_value == :unpaid ->
|
||||
if is_active do
|
||||
"#{base_classes} btn-error btn-active"
|
||||
else
|
||||
"#{base_classes} btn"
|
||||
end
|
||||
|
||||
true ->
|
||||
"#{base_classes} btn-outline"
|
||||
end
|
||||
end
|
||||
|
||||
# Get CSS classes for boolean filter label based on current state
|
||||
defp boolean_filter_label_class(boolean_filters, custom_field_id, expected_value) do
|
||||
base_classes = "join-item btn btn-sm"
|
||||
current_value = Map.get(boolean_filters, to_string(custom_field_id))
|
||||
is_active = current_value == expected_value
|
||||
|
||||
cond do
|
||||
# All button (nil expected)
|
||||
expected_value == nil ->
|
||||
if is_active do
|
||||
"#{base_classes} btn-active"
|
||||
else
|
||||
"#{base_classes} btn"
|
||||
end
|
||||
|
||||
# True button
|
||||
expected_value == true ->
|
||||
if is_active do
|
||||
"#{base_classes} btn-success btn-active"
|
||||
else
|
||||
"#{base_classes} btn"
|
||||
end
|
||||
|
||||
# False button
|
||||
expected_value == false ->
|
||||
if is_active do
|
||||
"#{base_classes} btn-error btn-active"
|
||||
else
|
||||
"#{base_classes} btn"
|
||||
end
|
||||
|
||||
true ->
|
||||
"#{base_classes} btn-outline"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
defmodule MvWeb.Components.PaymentFilterComponent do
|
||||
@moduledoc """
|
||||
Provides the PaymentFilter Live-Component.
|
||||
|
||||
A dropdown filter for filtering members by cycle payment status (paid/unpaid/all).
|
||||
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
|
||||
- `:cycle_status_filter` - Current filter state: `nil` (all), `:paid`, or `:unpaid`
|
||||
- `:id` - Component ID (required)
|
||||
- `:member_count` - Number of filtered members to display in badge (optional, default: 0)
|
||||
|
||||
## Events
|
||||
- Sends `{:payment_filter_changed, filter}` to parent when filter changes
|
||||
"""
|
||||
use MvWeb, :live_component
|
||||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
{:ok, assign(socket, :open, false)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
socket =
|
||||
socket
|
||||
|> assign(:id, assigns.id)
|
||||
|> assign(:cycle_status_filter, assigns[:cycle_status_filter])
|
||||
|> assign(:member_count, assigns[:member_count] || 0)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
class="relative"
|
||||
id={@id}
|
||||
phx-window-keydown={@open && "close_dropdown"}
|
||||
phx-key="Escape"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class={[
|
||||
"btn gap-2",
|
||||
@cycle_status_filter && "btn-active"
|
||||
]}
|
||||
phx-click="toggle_dropdown"
|
||||
phx-target={@myself}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={to_string(@open)}
|
||||
aria-label={gettext("Filter by payment status")}
|
||||
>
|
||||
<.icon name="hero-funnel" class="h-5 w-5" />
|
||||
<span class="hidden sm:inline">{filter_label(@cycle_status_filter)}</span>
|
||||
<span :if={@cycle_status_filter} class="badge badge-primary badge-sm">{@member_count}</span>
|
||||
</button>
|
||||
|
||||
<ul
|
||||
:if={@open}
|
||||
class="menu dropdown-content bg-base-100 rounded-box z-10 w-52 p-2 shadow-lg absolute right-0 mt-2"
|
||||
role="menu"
|
||||
aria-label={gettext("Payment filter")}
|
||||
phx-click-away="close_dropdown"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<li role="none">
|
||||
<button
|
||||
type="button"
|
||||
role="menuitemradio"
|
||||
aria-checked={to_string(@cycle_status_filter == nil)}
|
||||
class={@cycle_status_filter == nil && "active"}
|
||||
phx-click="select_filter"
|
||||
phx-value-filter=""
|
||||
phx-target={@myself}
|
||||
>
|
||||
<.icon name="hero-users" class="h-4 w-4" />
|
||||
{gettext("All")}
|
||||
</button>
|
||||
</li>
|
||||
<li role="none">
|
||||
<button
|
||||
type="button"
|
||||
role="menuitemradio"
|
||||
aria-checked={to_string(@cycle_status_filter == :paid)}
|
||||
class={@cycle_status_filter == :paid && "active"}
|
||||
phx-click="select_filter"
|
||||
phx-value-filter="paid"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<.icon name="hero-check-circle" class="h-4 w-4 text-success" />
|
||||
{gettext("Paid")}
|
||||
</button>
|
||||
</li>
|
||||
<li role="none">
|
||||
<button
|
||||
type="button"
|
||||
role="menuitemradio"
|
||||
aria-checked={to_string(@cycle_status_filter == :unpaid)}
|
||||
class={@cycle_status_filter == :unpaid && "active"}
|
||||
phx-click="select_filter"
|
||||
phx-value-filter="unpaid"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<.icon name="hero-x-circle" class="h-4 w-4 text-error" />
|
||||
{gettext("Unpaid")}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("toggle_dropdown", _params, socket) do
|
||||
{:noreply, assign(socket, :open, !socket.assigns.open)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("close_dropdown", _params, socket) do
|
||||
{:noreply, assign(socket, :open, false)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("select_filter", %{"filter" => filter_str}, socket) do
|
||||
filter = parse_filter(filter_str)
|
||||
|
||||
# Close dropdown and notify parent
|
||||
socket = assign(socket, :open, false)
|
||||
send(self(), {:payment_filter_changed, filter})
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
# Parse filter string to atom
|
||||
defp parse_filter("paid"), do: :paid
|
||||
defp parse_filter("unpaid"), do: :unpaid
|
||||
defp parse_filter(_), do: nil
|
||||
|
||||
# Get display label for current filter
|
||||
defp filter_label(nil), do: gettext("All")
|
||||
defp filter_label(:paid), do: gettext("Paid")
|
||||
defp filter_label(:unpaid), do: gettext("Unpaid")
|
||||
end
|
||||
|
|
@ -396,6 +396,44 @@ defmodule MvWeb.MemberLive.Index do
|
|||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:boolean_filter_changed, custom_field_id_str, filter_value}, socket) do
|
||||
# Update boolean filters map
|
||||
updated_filters =
|
||||
if filter_value == nil do
|
||||
# Remove filter if nil (All option selected)
|
||||
Map.delete(socket.assigns.boolean_custom_field_filters, custom_field_id_str)
|
||||
else
|
||||
# Add or update filter
|
||||
Map.put(socket.assigns.boolean_custom_field_filters, custom_field_id_str, filter_value)
|
||||
end
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:boolean_custom_field_filters, updated_filters)
|
||||
|> load_members()
|
||||
|> update_selection_assigns()
|
||||
|
||||
# Build the URL with all params including new filter
|
||||
query_params =
|
||||
build_query_params(
|
||||
socket.assigns.query,
|
||||
socket.assigns.sort_field,
|
||||
socket.assigns.sort_order,
|
||||
socket.assigns.cycle_status_filter,
|
||||
socket.assigns.show_current_cycle,
|
||||
updated_filters
|
||||
)
|
||||
|
||||
new_path = ~p"/members?#{query_params}"
|
||||
|
||||
{:noreply,
|
||||
push_patch(socket,
|
||||
to: new_path,
|
||||
replace: true
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:field_toggled, field_string, visible}, socket) do
|
||||
# Update user field selection
|
||||
|
|
|
|||
|
|
@ -37,9 +37,11 @@
|
|||
placeholder={gettext("Search...")}
|
||||
/>
|
||||
<.live_component
|
||||
module={MvWeb.Components.PaymentFilterComponent}
|
||||
id="payment-filter"
|
||||
module={MvWeb.Components.MemberFilterComponent}
|
||||
id="member-filter"
|
||||
cycle_status_filter={@cycle_status_filter}
|
||||
boolean_custom_fields={@boolean_custom_fields}
|
||||
boolean_filters={@boolean_custom_field_filters}
|
||||
member_count={length(@members)}
|
||||
/>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ msgstr "Hausnummer"
|
|||
msgid "Notes"
|
||||
msgstr "Notizen"
|
||||
|
||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#: lib/mv_web/live/member_live/show.ex
|
||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
||||
|
|
@ -182,6 +182,7 @@ msgstr "Speichern..."
|
|||
msgid "Street"
|
||||
msgstr "Straße"
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||
#: lib/mv_web/live/member_field_live/index_component.ex
|
||||
#: lib/mv_web/live/member_live/index/formatter.ex
|
||||
|
|
@ -196,6 +197,7 @@ msgstr "Nein"
|
|||
msgid "Show Member"
|
||||
msgstr "Mitglied anzeigen"
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||
#: lib/mv_web/live/member_field_live/index_component.ex
|
||||
#: lib/mv_web/live/member_live/index/formatter.ex
|
||||
|
|
@ -578,6 +580,7 @@ msgstr "E-Mail kann nicht aktualisiert werden: Diese E-Mail-Adresse ist bereits
|
|||
msgid "This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account."
|
||||
msgstr "Diese E-Mail-Adresse ist bereits mit einem anderen OIDC-Konto verknüpft. Es können nicht mehrere OIDC-Provider mit demselben Konto verknüpft werden."
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#: lib/mv_web/live/member_live/form.ex
|
||||
#: lib/mv_web/live/member_live/show.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
|
|
@ -739,21 +742,11 @@ msgid "This field cannot be empty"
|
|||
msgstr "Dieses Feld darf nicht leer bleiben"
|
||||
|
||||
#: lib/mv_web/components/core_components.ex
|
||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Filter by payment status"
|
||||
msgstr "Nach Zahlungsstatus filtern"
|
||||
|
||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Payment filter"
|
||||
msgstr "Zahlungsfilter"
|
||||
|
||||
#: lib/mv_web/live/member_live/show.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Address"
|
||||
|
|
@ -786,6 +779,7 @@ msgstr "Nr."
|
|||
msgid "Payment Data"
|
||||
msgstr "Beitragsdaten"
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#: lib/mv_web/live/member_live/form.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Payments"
|
||||
|
|
@ -920,7 +914,7 @@ msgstr "Status"
|
|||
msgid "Suspended"
|
||||
msgstr "Pausiert"
|
||||
|
||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#: lib/mv_web/live/member_live/show.ex
|
||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
||||
|
|
@ -1926,289 +1920,37 @@ msgstr "Validierung fehlgeschlagen: %{field} %{message}"
|
|||
msgid "Validation failed: %{message}"
|
||||
msgstr "Validierung fehlgeschlagen: %{message}"
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||
#~ msgid "Use this form to manage Custom Field Value records in your database."
|
||||
#~ msgstr "Verwende dieses Formular, um Benutzerdefinierte Feldwerte in deiner Datenbank zu verwalten."
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Close"
|
||||
msgstr "Schließen"
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Member"
|
||||
#~ msgstr "Mitglied"
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Filter by %{name}"
|
||||
msgstr "Filtern nach %{name}"
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Choose a custom field"
|
||||
#~ msgstr "Wähle ein Benutzerdefiniertes Feld"
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Filter members"
|
||||
msgstr "Mitglieder filtern"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Joining year - reduced to 0"
|
||||
#~ msgstr "Beitrittsjahr – auf 0 reduziert"
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Member filter"
|
||||
msgstr "Mitgliedsfilter"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Regular"
|
||||
#~ msgstr "Regulär"
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Payment Status"
|
||||
msgstr "Bezahlstatus"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Current"
|
||||
#~ msgstr "Aktuell"
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Payment status filter"
|
||||
msgstr "Bezahlstatusfilter"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Paid via bank transfer"
|
||||
#~ msgstr "Bezahlt durch Überweisung"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Mark as Unpaid"
|
||||
#~ msgstr "Als unbezahlt markieren"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Half-yearly contribution for supporting members"
|
||||
#~ msgstr "Halbjährlicher Beitrag für Fördermitglieder"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Reduced fee for unemployed, pensioners, or low income"
|
||||
#~ msgstr "Ermäßigter Beitrag für Arbeitslose, Rentner*innen oder Geringverdienende"
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_value_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Custom field value not found"
|
||||
#~ msgstr "Benutzerdefinierter Feldwert nicht gefunden"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Supporting Member"
|
||||
#~ msgstr "Fördermitglied"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Monthly fee for students and trainees"
|
||||
#~ msgstr "Monatlicher Beitrag für Studierende und Auszubildende"
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Custom field value %{action} successfully"
|
||||
#~ msgstr "Benutzerdefinierter Feldwert erfolgreich %{action}"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Total Contributions"
|
||||
#~ msgstr "Gesamtbeiträge"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Manage contribution types for membership fees."
|
||||
#~ msgstr "Beitragsarten für Mitgliedsbeiträge verwalten."
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Change Contribution Type"
|
||||
#~ msgstr "Beitragsart ändern"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||
#~ msgid "New Contribution Type"
|
||||
#~ msgstr "Neue Beitragsart"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Time Period"
|
||||
#~ msgstr "Zeitraum"
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_value_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Custom field value deleted successfully"
|
||||
#~ msgstr "Benutzerdefinierter Feldwert erfolgreich gelöscht"
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_value_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "You do not have permission to access this custom field value"
|
||||
#~ msgstr "Sie haben keine Berechtigung, auf diesen benutzerdefinierten Feldwert zuzugreifen"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Cannot delete - members assigned"
|
||||
#~ msgstr "Löschen nicht möglich – es sind Mitglieder zugewiesen"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Preview Mockup"
|
||||
#~ msgstr "Vorschau"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||
#~ msgid "Contribution Types"
|
||||
#~ msgstr "Beitragsarten"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "This page is not functional and only displays the planned features."
|
||||
#~ msgstr "Diese Seite ist nicht funktionsfähig und zeigt nur geplante Funktionen."
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||
#~ msgid "Member since"
|
||||
#~ msgstr "Mitglied seit"
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Unsupported value type: %{type}"
|
||||
#~ msgstr "Nicht unterstützter Wertetyp: %{type}"
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||
#~ msgid "Custom field"
|
||||
#~ msgstr "Benutzerdefinierte Felder"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Mark as Paid"
|
||||
#~ msgstr "Als bezahlt markieren"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||
#~ msgid "Contribution type"
|
||||
#~ msgstr "Beitragsart"
|
||||
|
||||
#~ #: lib/mv_web/components/layouts/sidebar.ex
|
||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||
#~ msgid "Contributions"
|
||||
#~ msgstr "Beiträge"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Reduced"
|
||||
#~ msgstr "Reduziert"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "No fee for honorary members"
|
||||
#~ msgstr "Kein Beitrag für ehrenamtliche Mitglieder"
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_value_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "You do not have permission to delete this custom field value"
|
||||
#~ msgstr "Sie haben keine Berechtigung, diesen benutzerdefinierten Feldwert zu löschen"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "%{count} period selected"
|
||||
#~ msgid_plural "%{count} periods selected"
|
||||
#~ msgstr[0] "%{count} Zyklus ausgewählt"
|
||||
#~ msgstr[1] "%{count} Zyklen ausgewählt"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Mark as Suspended"
|
||||
#~ msgstr "Als pausiert markieren"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||
#~ msgid "Contribution types define different membership fee structures. Each type has a fixed cycle (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
|
||||
#~ msgstr "Beitragsarten definieren verschiedene Beitragsmodelle. Jede Art hat einen festen Zyklus (monatlich, vierteljährlich, halbjährlich, jährlich), der nach Erstellung nicht mehr geändert werden kann."
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Choose a member"
|
||||
#~ msgstr "Mitglied auswählen"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Suspend"
|
||||
#~ msgstr "Pausieren"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Reopen"
|
||||
#~ msgstr "Wieder öffnen"
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Value"
|
||||
#~ msgstr "Wert"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Why are not all contribution types shown?"
|
||||
#~ msgstr "Warum werden nicht alle Beitragsarten angezeigt?"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||
#~ msgid "Contribution Start"
|
||||
#~ msgstr "Beitragsbeginn"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Standard membership fee for regular members"
|
||||
#~ msgstr "Regulärer Mitgliedsbeitrag für Vollmitglieder"
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||
#~ msgid "Save Custom Field Value"
|
||||
#~ msgstr "Benutzerdefinierten Feldwert speichern"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Honorary"
|
||||
#~ msgstr "Ehrenamtlich"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||
#~ msgid "Contributions for %{name}"
|
||||
#~ msgstr "Beiträge für %{name}"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Family"
|
||||
#~ msgstr "Familie"
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_value_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "You do not have permission to view custom field values"
|
||||
#~ msgstr "Sie haben keine Berechtigung, benutzerdefinierte Feldwerte anzuzeigen"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Student"
|
||||
#~ msgstr "Student"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Quarterly fee for family memberships"
|
||||
#~ msgstr "Vierteljährlicher Beitrag für Familienmitgliedschaften"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Members can only switch between contribution types with the same payment interval (e.g., yearly to yearly). This prevents complex period overlaps."
|
||||
#~ msgstr "Mitglieder können nur zwischen Beitragsarten mit demselben Zahlungszyklus wechseln (z. B. jährlich zu jährlich). Dadurch werden komplexe Überlappungen vermieden."
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Please select a custom field first"
|
||||
#~ msgstr "Bitte wähle zuerst ein Benutzerdefiniertes Feld"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Open Contributions"
|
||||
#~ msgstr "Offene Beiträge"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Member Contributions"
|
||||
#~ msgstr "Mitgliedsbeiträge"
|
||||
|
||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "About Contribution Types"
|
||||
#~ msgstr "Über Beitragsarten"
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Reset"
|
||||
msgstr "Zurücksetzen"
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ msgstr ""
|
|||
msgid "Notes"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#: lib/mv_web/live/member_live/show.ex
|
||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
||||
|
|
@ -183,6 +183,7 @@ msgstr ""
|
|||
msgid "Street"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||
#: lib/mv_web/live/member_field_live/index_component.ex
|
||||
#: lib/mv_web/live/member_live/index/formatter.ex
|
||||
|
|
@ -197,6 +198,7 @@ msgstr ""
|
|||
msgid "Show Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||
#: lib/mv_web/live/member_field_live/index_component.ex
|
||||
#: lib/mv_web/live/member_live/index/formatter.ex
|
||||
|
|
@ -579,6 +581,7 @@ msgstr ""
|
|||
msgid "This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#: lib/mv_web/live/member_live/form.ex
|
||||
#: lib/mv_web/live/member_live/show.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
|
|
@ -740,21 +743,11 @@ msgid "This field cannot be empty"
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/core_components.ex
|
||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Filter by payment status"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Payment filter"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/show.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Address"
|
||||
|
|
@ -787,6 +780,7 @@ msgstr ""
|
|||
msgid "Payment Data"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#: lib/mv_web/live/member_live/form.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Payments"
|
||||
|
|
@ -921,7 +915,7 @@ msgstr ""
|
|||
msgid "Suspended"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#: lib/mv_web/live/member_live/show.ex
|
||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
||||
|
|
@ -1926,3 +1920,38 @@ msgstr ""
|
|||
#, elixir-autogen, elixir-format
|
||||
msgid "Validation failed: %{message}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Filter by %{name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Filter members"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Member filter"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Payment Status"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Payment status filter"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Reset"
|
||||
msgstr ""
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ msgstr ""
|
|||
msgid "Notes"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#: lib/mv_web/live/member_live/show.ex
|
||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
||||
|
|
@ -183,6 +183,7 @@ msgstr ""
|
|||
msgid "Street"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||
#: lib/mv_web/live/member_field_live/index_component.ex
|
||||
#: lib/mv_web/live/member_live/index/formatter.ex
|
||||
|
|
@ -197,6 +198,7 @@ msgstr ""
|
|||
msgid "Show Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||
#: lib/mv_web/live/member_field_live/index_component.ex
|
||||
#: lib/mv_web/live/member_live/index/formatter.ex
|
||||
|
|
@ -579,6 +581,7 @@ msgstr ""
|
|||
msgid "This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#: lib/mv_web/live/member_live/form.ex
|
||||
#: lib/mv_web/live/member_live/show.ex
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
|
|
@ -740,21 +743,11 @@ msgid "This field cannot be empty"
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/core_components.ex
|
||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Filter by payment status"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Payment filter"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/show.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Address"
|
||||
|
|
@ -921,7 +914,7 @@ msgstr ""
|
|||
msgid "Suspended"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#: lib/mv_web/live/member_live/show.ex
|
||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
||||
|
|
@ -1927,16 +1920,51 @@ msgstr ""
|
|||
msgid "Validation failed: %{message}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Filter by %{name}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Filter members"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Member filter"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Payment"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Payment Status"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Payment status filter"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/member_filter_component.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Reset"
|
||||
msgstr ""
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||
#~ msgid "Use this form to manage Custom Field Value records in your database."
|
||||
#~ msgstr ""
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Member"
|
||||
#~ msgstr ""
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Choose a custom field"
|
||||
|
|
@ -1998,6 +2026,11 @@ msgstr ""
|
|||
#~ msgid "Monthly fee for students and trainees"
|
||||
#~ msgstr ""
|
||||
|
||||
#~ #: lib/mv_web/live/components/payment_filter_component.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Filter by payment status"
|
||||
#~ msgstr ""
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "Custom field value %{action} successfully"
|
||||
|
|
|
|||
267
test/mv_web/components/member_filter_component_test.exs
Normal file
267
test/mv_web/components/member_filter_component_test.exs
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
defmodule MvWeb.Components.MemberFilterComponentTest do
|
||||
@moduledoc """
|
||||
Unit tests for the MemberFilterComponent.
|
||||
|
||||
Tests cover:
|
||||
- Rendering Payment Filter and Boolean Custom Fields
|
||||
- Boolean filter selection and event emission
|
||||
- Button label and badge logic
|
||||
- Filtering to show only boolean custom fields
|
||||
"""
|
||||
# async: false to prevent PostgreSQL deadlocks when running LiveView tests against DB
|
||||
use MvWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
use Gettext, backend: MvWeb.Gettext
|
||||
|
||||
alias Mv.Membership.CustomField
|
||||
|
||||
# Helper to create a boolean custom field
|
||||
defp create_boolean_custom_field(attrs \\ %{}) do
|
||||
default_attrs = %{
|
||||
name: "test_boolean_#{System.unique_integer([:positive])}",
|
||||
value_type: :boolean
|
||||
}
|
||||
|
||||
attrs = Map.merge(default_attrs, attrs)
|
||||
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
end
|
||||
|
||||
# Helper to create a non-boolean custom field
|
||||
defp create_string_custom_field(attrs \\ %{}) do
|
||||
default_attrs = %{
|
||||
name: "test_string_#{System.unique_integer([:positive])}",
|
||||
value_type: :string
|
||||
}
|
||||
|
||||
attrs = Map.merge(default_attrs, attrs)
|
||||
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, attrs)
|
||||
|> Ash.create!()
|
||||
end
|
||||
|
||||
describe "rendering" do
|
||||
test "renders boolean custom fields when present", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
boolean_field = create_boolean_custom_field(%{name: "Active Member"})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Should show the boolean custom field name in the dropdown
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
assert html =~ boolean_field.name
|
||||
end
|
||||
|
||||
test "renders payment and custom fields groups when boolean fields exist", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
_boolean_field = create_boolean_custom_field()
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
# Should have both "Payments" and "Custom Fields" group labels
|
||||
assert html =~ gettext("Payments") || html =~ "Payment"
|
||||
assert html =~ gettext("Custom Fields")
|
||||
end
|
||||
|
||||
test "renders only payment filter when no boolean custom fields exist", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
# Create a non-boolean field to ensure it's not shown
|
||||
_string_field = create_string_custom_field()
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Component should exist with correct ID
|
||||
assert has_element?(view, "#member-filter")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Should show payment filter options (check both English and translated)
|
||||
assert html =~ "All" || html =~ gettext("All")
|
||||
assert html =~ "Paid" || html =~ gettext("Paid")
|
||||
assert html =~ "Unpaid" || html =~ gettext("Unpaid")
|
||||
|
||||
# Should not show any boolean field names (since none exist)
|
||||
# We can't easily check this without knowing field names, but the structure should be correct
|
||||
end
|
||||
end
|
||||
|
||||
describe "boolean filter selection" do
|
||||
test "selecting boolean filter sends correct event", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
boolean_field = create_boolean_custom_field(%{name: "Newsletter"})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
# Select "True" option for the boolean field using radio input
|
||||
# Radio inputs trigger phx-change on the form, so we use render_change on the form
|
||||
view
|
||||
|> form("#member-filter form", %{
|
||||
"custom_boolean" => %{to_string(boolean_field.id) => "true"}
|
||||
})
|
||||
|> render_change()
|
||||
|
||||
# The event should be sent to the parent LiveView
|
||||
# We verify this by checking that the URL is updated
|
||||
assert_patch(view)
|
||||
end
|
||||
|
||||
test "payment filter still works after component extension", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
_boolean_field = create_boolean_custom_field()
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
# Select "Paid" option using radio input
|
||||
# Radio inputs trigger phx-change on the form, so we use render_change on the form
|
||||
view
|
||||
|> form("#member-filter form", %{"payment_filter" => "paid"})
|
||||
|> render_change()
|
||||
|
||||
# URL should be updated with cycle_status_filter=paid
|
||||
path = assert_patch(view)
|
||||
assert path =~ "cycle_status_filter=paid"
|
||||
end
|
||||
end
|
||||
|
||||
describe "button label" do
|
||||
test "shows active boolean filter names in button label", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
boolean_field1 = create_boolean_custom_field(%{name: "Active Member"})
|
||||
boolean_field2 = create_boolean_custom_field(%{name: "Newsletter"})
|
||||
|
||||
# Set filters via URL
|
||||
{:ok, view, _html} =
|
||||
live(
|
||||
conn,
|
||||
"/members?bf_#{boolean_field1.id}=true&bf_#{boolean_field2.id}=false"
|
||||
)
|
||||
|
||||
# Component should exist
|
||||
assert has_element?(view, "#member-filter")
|
||||
|
||||
# Button label should contain the custom field names
|
||||
# The exact format depends on implementation, but should show active filters
|
||||
button_html =
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render()
|
||||
|
||||
assert button_html =~ boolean_field1.name || button_html =~ boolean_field2.name
|
||||
end
|
||||
|
||||
test "truncates long custom field names in button label", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
# Create field with very long name (>30 characters)
|
||||
long_name = String.duplicate("A", 50)
|
||||
boolean_field = create_boolean_custom_field(%{name: long_name})
|
||||
|
||||
# Set filter via URL
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?bf_#{boolean_field.id}=true")
|
||||
|
||||
# Component should exist
|
||||
assert has_element?(view, "#member-filter")
|
||||
|
||||
# Get button label text
|
||||
button_html =
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render()
|
||||
|
||||
# Button label should be truncated - full name should NOT appear in button
|
||||
# (it may appear in dropdown when opened, but not in the button label itself)
|
||||
# Check that button doesn't contain the full 50-character name
|
||||
refute button_html =~ long_name
|
||||
|
||||
# Button should still contain some text (truncated version or indicator)
|
||||
assert String.length(button_html) > 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "badge" do
|
||||
test "shows total count of active boolean filters in badge", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
boolean_field1 = create_boolean_custom_field(%{name: "Field1"})
|
||||
boolean_field2 = create_boolean_custom_field(%{name: "Field2"})
|
||||
|
||||
# Set two filters via URL
|
||||
{:ok, view, _html} =
|
||||
live(
|
||||
conn,
|
||||
"/members?bf_#{boolean_field1.id}=true&bf_#{boolean_field2.id}=false"
|
||||
)
|
||||
|
||||
# Component should exist
|
||||
assert has_element?(view, "#member-filter")
|
||||
|
||||
# Badge should be visible when boolean filters are active
|
||||
assert has_element?(view, "#member-filter .badge")
|
||||
|
||||
# Badge should show count of active boolean filters (2 in this case)
|
||||
badge_html =
|
||||
view
|
||||
|> element("#member-filter .badge")
|
||||
|> render()
|
||||
|
||||
assert badge_html =~ "2"
|
||||
end
|
||||
end
|
||||
|
||||
describe "filtering" do
|
||||
test "only boolean custom fields are displayed, not string or integer fields", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
boolean_field = create_boolean_custom_field(%{name: "Boolean Field"})
|
||||
_string_field = create_string_custom_field(%{name: "String Field"})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
# Should show boolean field in the dropdown panel
|
||||
# Extract only the dropdown panel HTML to check
|
||||
dropdown_html =
|
||||
view
|
||||
|> element("#member-filter div[role='dialog']")
|
||||
|> render()
|
||||
|
||||
# Should show boolean field in dropdown
|
||||
assert dropdown_html =~ boolean_field.name
|
||||
|
||||
# Should not show string field name in the filter dropdown
|
||||
# (String fields should not appear in boolean filter section)
|
||||
refute dropdown_html =~ "String Field"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,183 +0,0 @@
|
|||
defmodule MvWeb.Components.PaymentFilterComponentTest do
|
||||
@moduledoc """
|
||||
Unit tests for the PaymentFilterComponent.
|
||||
|
||||
Tests cover:
|
||||
- Rendering in all 3 filter states (nil, :paid, :unpaid)
|
||||
- Event emission when selecting options
|
||||
- ARIA attributes for accessibility
|
||||
- Dropdown open/close behavior
|
||||
"""
|
||||
# async: false to prevent PostgreSQL deadlocks when running LiveView tests against DB
|
||||
use MvWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
describe "rendering" do
|
||||
test "renders with no filter active (nil)", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Should show "All" text and no badge
|
||||
assert has_element?(view, "#payment-filter")
|
||||
refute has_element?(view, "#payment-filter .badge")
|
||||
end
|
||||
|
||||
test "renders with paid filter active", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?cycle_status_filter=paid")
|
||||
|
||||
# Should show badge when filter is active
|
||||
assert has_element?(view, "#payment-filter .badge")
|
||||
end
|
||||
|
||||
test "renders with unpaid filter active", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?cycle_status_filter=unpaid")
|
||||
|
||||
# Should show badge when filter is active
|
||||
assert has_element?(view, "#payment-filter .badge")
|
||||
end
|
||||
end
|
||||
|
||||
describe "dropdown behavior" do
|
||||
test "dropdown opens on button click", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Initially dropdown is closed
|
||||
refute has_element?(view, "#payment-filter ul[role='menu']")
|
||||
|
||||
# Click to open
|
||||
view
|
||||
|> element("#payment-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
# Dropdown should be visible
|
||||
assert has_element?(view, "#payment-filter ul[role='menu']")
|
||||
end
|
||||
|
||||
test "dropdown closes after selecting an option", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#payment-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, "#payment-filter ul[role='menu']")
|
||||
|
||||
# Select an option - this should close the dropdown
|
||||
view
|
||||
|> element("#payment-filter button[phx-value-filter='paid']")
|
||||
|> render_click()
|
||||
|
||||
# After selection, dropdown should be closed
|
||||
# Note: The dropdown closes via assign, which is reflected in the next render
|
||||
refute has_element?(view, "#payment-filter ul[role='menu']")
|
||||
end
|
||||
end
|
||||
|
||||
describe "filter selection" do
|
||||
test "selecting 'All' clears the filter and updates URL", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?cycle_status_filter=paid")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#payment-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
# Select "All" option
|
||||
view
|
||||
|> element("#payment-filter button[phx-value-filter='']")
|
||||
|> render_click()
|
||||
|
||||
# URL should not contain cycle_status_filter param - wait for patch
|
||||
assert_patch(view)
|
||||
end
|
||||
|
||||
test "selecting 'Paid' sets the filter and updates URL", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#payment-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
# Select "Paid" option
|
||||
view
|
||||
|> element("#payment-filter button[phx-value-filter='paid']")
|
||||
|> render_click()
|
||||
|
||||
# Wait for patch and check URL contains cycle_status_filter=paid
|
||||
path = assert_patch(view)
|
||||
assert path =~ "cycle_status_filter=paid"
|
||||
end
|
||||
|
||||
test "selecting 'Unpaid' sets the filter and updates URL", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#payment-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
# Select "Unpaid" option
|
||||
view
|
||||
|> element("#payment-filter button[phx-value-filter='unpaid']")
|
||||
|> render_click()
|
||||
|
||||
# Wait for patch and check URL contains cycle_status_filter=unpaid
|
||||
path = assert_patch(view)
|
||||
assert path =~ "cycle_status_filter=unpaid"
|
||||
end
|
||||
end
|
||||
|
||||
describe "accessibility" do
|
||||
test "has correct ARIA attributes", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, html} = live(conn, "/members")
|
||||
|
||||
# Main button should have aria-haspopup and aria-expanded
|
||||
assert html =~ ~s(aria-haspopup="true")
|
||||
assert html =~ ~s(aria-expanded="false")
|
||||
assert html =~ ~s(aria-label=)
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#payment-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Check aria-expanded is now true
|
||||
assert html =~ ~s(aria-expanded="true")
|
||||
|
||||
# Menu should have role="menu"
|
||||
assert html =~ ~s(role="menu")
|
||||
|
||||
# Options should have role="menuitemradio"
|
||||
assert html =~ ~s(role="menuitemradio")
|
||||
end
|
||||
|
||||
test "has aria-checked on selected option", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?cycle_status_filter=paid")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#payment-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
|
||||
# "Paid" option should have aria-checked="true"
|
||||
# Check both possible orderings of attributes
|
||||
assert html =~ "aria-checked=\"true\"" and html =~ "phx-value-filter=\"paid\""
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue