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
|
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
|
@impl true
|
||||||
def handle_info({:field_toggled, field_string, visible}, socket) do
|
def handle_info({:field_toggled, field_string, visible}, socket) do
|
||||||
# Update user field selection
|
# Update user field selection
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,11 @@
|
||||||
placeholder={gettext("Search...")}
|
placeholder={gettext("Search...")}
|
||||||
/>
|
/>
|
||||||
<.live_component
|
<.live_component
|
||||||
module={MvWeb.Components.PaymentFilterComponent}
|
module={MvWeb.Components.MemberFilterComponent}
|
||||||
id="payment-filter"
|
id="member-filter"
|
||||||
cycle_status_filter={@cycle_status_filter}
|
cycle_status_filter={@cycle_status_filter}
|
||||||
|
boolean_custom_fields={@boolean_custom_fields}
|
||||||
|
boolean_filters={@boolean_custom_field_filters}
|
||||||
member_count={length(@members)}
|
member_count={length(@members)}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ msgstr "Hausnummer"
|
||||||
msgid "Notes"
|
msgid "Notes"
|
||||||
msgstr "Notizen"
|
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.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
|
||||||
|
|
@ -182,6 +182,7 @@ msgstr "Speichern..."
|
||||||
msgid "Street"
|
msgid "Street"
|
||||||
msgstr "Straße"
|
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/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_field_live/index_component.ex
|
#: lib/mv_web/live/member_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex
|
#: lib/mv_web/live/member_live/index/formatter.ex
|
||||||
|
|
@ -196,6 +197,7 @@ msgstr "Nein"
|
||||||
msgid "Show Member"
|
msgid "Show Member"
|
||||||
msgstr "Mitglied anzeigen"
|
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/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_field_live/index_component.ex
|
#: lib/mv_web/live/member_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/index/formatter.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."
|
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."
|
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/form.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
|
||||||
|
|
@ -739,21 +742,11 @@ 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/components/payment_filter_component.ex
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr "Alle"
|
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
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Address"
|
msgid "Address"
|
||||||
|
|
@ -786,6 +779,7 @@ msgstr "Nr."
|
||||||
msgid "Payment Data"
|
msgid "Payment Data"
|
||||||
msgstr "Beitragsdaten"
|
msgstr "Beitragsdaten"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Payments"
|
msgid "Payments"
|
||||||
|
|
@ -920,7 +914,7 @@ msgstr "Status"
|
||||||
msgid "Suspended"
|
msgid "Suspended"
|
||||||
msgstr "Pausiert"
|
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.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
|
||||||
|
|
@ -1926,289 +1920,37 @@ msgstr "Validierung fehlgeschlagen: %{field} %{message}"
|
||||||
msgid "Validation failed: %{message}"
|
msgid "Validation failed: %{message}"
|
||||||
msgstr "Validierung fehlgeschlagen: %{message}"
|
msgstr "Validierung fehlgeschlagen: %{message}"
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
#~ msgid "Use this form to manage Custom Field Value records in your database."
|
msgid "Close"
|
||||||
#~ msgstr "Verwende dieses Formular, um Benutzerdefinierte Feldwerte in deiner Datenbank zu verwalten."
|
msgstr "Schließen"
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
#~ msgid "Member"
|
msgid "Filter by %{name}"
|
||||||
#~ msgstr "Mitglied"
|
msgstr "Filtern nach %{name}"
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
#~ msgid "Choose a custom field"
|
msgid "Filter members"
|
||||||
#~ msgstr "Wähle ein Benutzerdefiniertes Feld"
|
msgstr "Mitglieder filtern"
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
#~ msgid "Joining year - reduced to 0"
|
msgid "Member filter"
|
||||||
#~ msgstr "Beitrittsjahr – auf 0 reduziert"
|
msgstr "Mitgliedsfilter"
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
#~ #, elixir-autogen, elixir-format
|
msgid "Payment Status"
|
||||||
#~ msgid "Regular"
|
msgstr "Bezahlstatus"
|
||||||
#~ msgstr "Regulär"
|
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
#~ msgid "Current"
|
msgid "Payment status filter"
|
||||||
#~ msgstr "Aktuell"
|
msgstr "Bezahlstatusfilter"
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
#~ msgid "Paid via bank transfer"
|
msgid "Reset"
|
||||||
#~ msgstr "Bezahlt durch Überweisung"
|
msgstr "Zurücksetzen"
|
||||||
|
|
||||||
#~ #: 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"
|
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ msgstr ""
|
||||||
msgid "Notes"
|
msgid "Notes"
|
||||||
msgstr ""
|
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.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
|
||||||
|
|
@ -183,6 +183,7 @@ msgstr ""
|
||||||
msgid "Street"
|
msgid "Street"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#: 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_field_live/index_component.ex
|
#: lib/mv_web/live/member_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex
|
#: lib/mv_web/live/member_live/index/formatter.ex
|
||||||
|
|
@ -197,6 +198,7 @@ msgstr ""
|
||||||
msgid "Show Member"
|
msgid "Show Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#: 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_field_live/index_component.ex
|
#: lib/mv_web/live/member_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/index/formatter.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."
|
msgid "This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.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
|
||||||
|
|
@ -740,21 +743,11 @@ 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/components/payment_filter_component.ex
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr ""
|
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
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Address"
|
msgid "Address"
|
||||||
|
|
@ -787,6 +780,7 @@ msgstr ""
|
||||||
msgid "Payment Data"
|
msgid "Payment Data"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Payments"
|
msgid "Payments"
|
||||||
|
|
@ -921,7 +915,7 @@ msgstr ""
|
||||||
msgid "Suspended"
|
msgid "Suspended"
|
||||||
msgstr ""
|
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.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
|
||||||
|
|
@ -1926,3 +1920,38 @@ msgstr ""
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Validation failed: %{message}"
|
msgid "Validation failed: %{message}"
|
||||||
msgstr ""
|
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"
|
msgid "Notes"
|
||||||
msgstr ""
|
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.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
|
||||||
|
|
@ -183,6 +183,7 @@ msgstr ""
|
||||||
msgid "Street"
|
msgid "Street"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#: 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_field_live/index_component.ex
|
#: lib/mv_web/live/member_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex
|
#: lib/mv_web/live/member_live/index/formatter.ex
|
||||||
|
|
@ -197,6 +198,7 @@ msgstr ""
|
||||||
msgid "Show Member"
|
msgid "Show Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#: 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_field_live/index_component.ex
|
#: lib/mv_web/live/member_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/index/formatter.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."
|
msgid "This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#: 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
|
||||||
|
|
@ -740,21 +743,11 @@ 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/components/payment_filter_component.ex
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr ""
|
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
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Address"
|
msgid "Address"
|
||||||
|
|
@ -921,7 +914,7 @@ msgstr ""
|
||||||
msgid "Suspended"
|
msgid "Suspended"
|
||||||
msgstr ""
|
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.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
|
||||||
|
|
@ -1927,16 +1920,51 @@ msgstr ""
|
||||||
msgid "Validation failed: %{message}"
|
msgid "Validation failed: %{message}"
|
||||||
msgstr ""
|
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
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
#~ msgid "Use this form to manage Custom Field Value records in your database."
|
#~ msgid "Use this form to manage Custom Field Value records in your database."
|
||||||
#~ msgstr ""
|
#~ 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
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "Choose a custom field"
|
#~ msgid "Choose a custom field"
|
||||||
|
|
@ -1998,6 +2026,11 @@ msgstr ""
|
||||||
#~ msgid "Monthly fee for students and trainees"
|
#~ msgid "Monthly fee for students and trainees"
|
||||||
#~ msgstr ""
|
#~ 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
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "Custom field value %{action} successfully"
|
#~ 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