feat(member-filter): add date filter sections with active-count badge and reset support
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
e3295ab4b5
commit
d6671daf1a
7 changed files with 834 additions and 18 deletions
|
|
@ -23,6 +23,11 @@ defmodule MvWeb.Components.MemberFilterComponent do
|
|||
- `:fee_type_filters` - Map of active fee type filters: `%{fee_type_id => :in | :not_in}` (nil = All).
|
||||
- `:boolean_custom_fields` - List of boolean custom fields to display
|
||||
- `:boolean_filters` - Map of active boolean filters: `%{custom_field_id => true | false}`
|
||||
- `:date_custom_fields` - List of date-typed custom fields rendered in the
|
||||
"Custom date fields" section (each with `:id`, `:name`, `:value_type`).
|
||||
- `:date_filters` - Date filter state map (see `MvWeb.MemberLive.Index.DateFilter`):
|
||||
built-in `:join_date` / `:exit_date` bounds and mode, plus optional
|
||||
UUID-keyed custom date field bound entries.
|
||||
- `:id` - Component ID (required)
|
||||
- `:member_count` - Number of filtered members to display in badge (optional, default: 0)
|
||||
|
||||
|
|
@ -31,13 +36,18 @@ defmodule MvWeb.Components.MemberFilterComponent do
|
|||
- Sends `{:group_filter_changed, group_id_str, value}` to parent when a group filter changes (value: nil | :in | :not_in)
|
||||
- Sends `{:fee_type_filter_changed, fee_type_id_str, value}` to parent when a fee type filter changes (value: nil | :in | :not_in)
|
||||
- Sends `{:boolean_filter_changed, custom_field_id, filter_value}` to parent when boolean filter changes
|
||||
- Sends `{:date_filters_changed, new_filters}` to parent when any date
|
||||
filter input changes (built-in date bounds, exit_date mode, or custom
|
||||
date field bounds).
|
||||
"""
|
||||
use MvWeb, :live_component
|
||||
|
||||
alias MvWeb.MemberLive.Index.DateFilter
|
||||
alias MvWeb.MemberLive.Index.FilterParams
|
||||
|
||||
@group_filter_prefix Mv.Constants.group_filter_prefix()
|
||||
@fee_type_filter_prefix Mv.Constants.fee_type_filter_prefix()
|
||||
@custom_date_filter_prefix Mv.Constants.custom_date_filter_prefix()
|
||||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
|
|
@ -50,19 +60,42 @@ defmodule MvWeb.Components.MemberFilterComponent do
|
|||
socket
|
||||
|> assign(:id, assigns.id)
|
||||
|> assign(:cycle_status_filter, assigns[:cycle_status_filter])
|
||||
|> assign(:groups, assigns[:groups] || [])
|
||||
|> assign(:group_filters, assigns[:group_filters] || %{})
|
||||
|> assign(:group_filter_prefix, @group_filter_prefix)
|
||||
|> assign(:fee_types, assigns[:fee_types] || [])
|
||||
|> assign(:fee_type_filters, assigns[:fee_type_filters] || %{})
|
||||
|> assign(:fee_type_filter_prefix, @fee_type_filter_prefix)
|
||||
|> assign(:boolean_custom_fields, assigns[:boolean_custom_fields] || [])
|
||||
|> assign(:boolean_filters, assigns[:boolean_filters] || %{})
|
||||
|> assign_group_assigns(assigns)
|
||||
|> assign_fee_type_assigns(assigns)
|
||||
|> assign_boolean_assigns(assigns)
|
||||
|> assign_date_assigns(assigns)
|
||||
|> assign(:member_count, assigns[:member_count] || 0)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
defp assign_group_assigns(socket, assigns) do
|
||||
socket
|
||||
|> assign(:groups, assigns[:groups] || [])
|
||||
|> assign(:group_filters, assigns[:group_filters] || %{})
|
||||
|> assign(:group_filter_prefix, @group_filter_prefix)
|
||||
end
|
||||
|
||||
defp assign_fee_type_assigns(socket, assigns) do
|
||||
socket
|
||||
|> assign(:fee_types, assigns[:fee_types] || [])
|
||||
|> assign(:fee_type_filters, assigns[:fee_type_filters] || %{})
|
||||
|> assign(:fee_type_filter_prefix, @fee_type_filter_prefix)
|
||||
end
|
||||
|
||||
defp assign_boolean_assigns(socket, assigns) do
|
||||
socket
|
||||
|> assign(:boolean_custom_fields, assigns[:boolean_custom_fields] || [])
|
||||
|> assign(:boolean_filters, assigns[:boolean_filters] || %{})
|
||||
end
|
||||
|
||||
defp assign_date_assigns(socket, assigns) do
|
||||
socket
|
||||
|> assign(:date_custom_fields, assigns[:date_custom_fields] || [])
|
||||
|> assign(:date_filters, assigns[:date_filters] || DateFilter.default())
|
||||
|> assign(:custom_date_filter_prefix, @custom_date_filter_prefix)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
|
|
@ -81,7 +114,8 @@ defmodule MvWeb.Components.MemberFilterComponent do
|
|||
"gap-2",
|
||||
(@cycle_status_filter || map_size(@group_filters) > 0 ||
|
||||
map_size(@fee_type_filters) > 0 ||
|
||||
active_boolean_filters_count(@boolean_filters) > 0) &&
|
||||
active_boolean_filters_count(@boolean_filters) > 0 ||
|
||||
date_filters_active?(@date_filters)) &&
|
||||
"btn-active"
|
||||
]}
|
||||
phx-click="toggle_dropdown"
|
||||
|
|
@ -99,7 +133,8 @@ defmodule MvWeb.Components.MemberFilterComponent do
|
|||
@fee_types,
|
||||
@fee_type_filters,
|
||||
@boolean_custom_fields,
|
||||
@boolean_filters
|
||||
@boolean_filters,
|
||||
@date_filters
|
||||
)}
|
||||
</span>
|
||||
<.badge
|
||||
|
|
@ -111,7 +146,9 @@ defmodule MvWeb.Components.MemberFilterComponent do
|
|||
</.badge>
|
||||
<.badge
|
||||
:if={
|
||||
(@cycle_status_filter || map_size(@group_filters) > 0 || map_size(@fee_type_filters) > 0) &&
|
||||
(@cycle_status_filter || map_size(@group_filters) > 0 ||
|
||||
map_size(@fee_type_filters) > 0 ||
|
||||
date_filters_active?(@date_filters)) &&
|
||||
active_boolean_filters_count(@boolean_filters) == 0
|
||||
}
|
||||
variant="primary"
|
||||
|
|
@ -329,6 +366,163 @@ defmodule MvWeb.Components.MemberFilterComponent do
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dates Group (built-in fields: exit_date with mode selector, join_date range) -->
|
||||
<div class="mb-4">
|
||||
<div class="text-xs font-semibold opacity-70 mb-2 uppercase tracking-wider">
|
||||
{gettext("Dates")}
|
||||
</div>
|
||||
<fieldset class="border-0 p-0 m-0 min-w-0 mb-3">
|
||||
<legend class="text-sm font-medium mb-1">
|
||||
{gettext("Exit date")}
|
||||
</legend>
|
||||
<div class="join w-full">
|
||||
<label
|
||||
class={"#{exit_mode_label_class(@date_filters, :active_only)} has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary"}
|
||||
for="ed-mode-active-only"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id="ed-mode-active-only"
|
||||
name="ed_mode"
|
||||
value="active_only"
|
||||
class="absolute opacity-0 w-0 h-0 pointer-events-none"
|
||||
checked={exit_mode(@date_filters) == :active_only}
|
||||
/>
|
||||
<span class="text-xs">{gettext("Active only")}</span>
|
||||
</label>
|
||||
<label
|
||||
class={"#{exit_mode_label_class(@date_filters, :all)} has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary"}
|
||||
for="ed-mode-all"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id="ed-mode-all"
|
||||
name="ed_mode"
|
||||
value="all"
|
||||
class="absolute opacity-0 w-0 h-0 pointer-events-none"
|
||||
checked={exit_mode(@date_filters) == :all}
|
||||
/>
|
||||
<span class="text-xs">{gettext("All")}</span>
|
||||
</label>
|
||||
<label
|
||||
class={"#{exit_mode_label_class(@date_filters, :inactive_only)} has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary"}
|
||||
for="ed-mode-inactive-only"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id="ed-mode-inactive-only"
|
||||
name="ed_mode"
|
||||
value="inactive_only"
|
||||
class="absolute opacity-0 w-0 h-0 pointer-events-none"
|
||||
checked={exit_mode(@date_filters) == :inactive_only}
|
||||
/>
|
||||
<span class="text-xs">{gettext("Inactive only")}</span>
|
||||
</label>
|
||||
<label
|
||||
class={"#{exit_mode_label_class(@date_filters, :custom)} has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary"}
|
||||
for="ed-mode-custom"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id="ed-mode-custom"
|
||||
name="ed_mode"
|
||||
value="custom"
|
||||
class="absolute opacity-0 w-0 h-0 pointer-events-none"
|
||||
checked={exit_mode(@date_filters) == :custom}
|
||||
/>
|
||||
<span class="text-xs">{gettext("Range")}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
:if={exit_mode(@date_filters) == :custom}
|
||||
class="mt-2 flex gap-3 items-end flex-wrap"
|
||||
>
|
||||
<.input
|
||||
type="date"
|
||||
id="ed-from"
|
||||
name="ed_from"
|
||||
label={gettext("From")}
|
||||
class="input input-sm input-bordered"
|
||||
aria-label={gettext("Exit date from")}
|
||||
value={date_value_for_input(@date_filters, :exit_date, :from)}
|
||||
/>
|
||||
<.input
|
||||
type="date"
|
||||
id="ed-to"
|
||||
name="ed_to"
|
||||
label={gettext("To")}
|
||||
class="input input-sm input-bordered"
|
||||
aria-label={gettext("Exit date to")}
|
||||
value={date_value_for_input(@date_filters, :exit_date, :to)}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="border-0 p-0 m-0 min-w-0">
|
||||
<legend class="text-sm font-medium mb-1">
|
||||
{gettext("Join date")}
|
||||
</legend>
|
||||
<div class="flex gap-3 items-end flex-wrap">
|
||||
<.input
|
||||
type="date"
|
||||
id="jd-from"
|
||||
name="jd_from"
|
||||
label={gettext("From")}
|
||||
class="input input-sm input-bordered"
|
||||
aria-label={gettext("Join date from")}
|
||||
value={date_value_for_input(@date_filters, :join_date, :from)}
|
||||
/>
|
||||
<.input
|
||||
type="date"
|
||||
id="jd-to"
|
||||
name="jd_to"
|
||||
label={gettext("To")}
|
||||
class="input input-sm input-bordered"
|
||||
aria-label={gettext("Join date to")}
|
||||
value={date_value_for_input(@date_filters, :join_date, :to)}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<!-- Custom Date Fields Group (in-memory filter; one row per :date custom field) -->
|
||||
<div :if={length(@date_custom_fields) > 0} class="mb-4">
|
||||
<div class="text-xs font-semibold opacity-70 mb-2 uppercase tracking-wider">
|
||||
{gettext("Custom date fields")}
|
||||
</div>
|
||||
<div class={
|
||||
if length(@date_custom_fields) > 5, do: "max-h-60 overflow-y-auto pr-2", else: ""
|
||||
}>
|
||||
<fieldset
|
||||
:for={field <- @date_custom_fields}
|
||||
class="border-0 border-b border-base-200 last:border-b-0 p-0 m-0 min-w-0 py-2"
|
||||
>
|
||||
<legend class="text-sm font-medium mb-1">
|
||||
{field.name}
|
||||
</legend>
|
||||
<div class="flex gap-3 items-end flex-wrap">
|
||||
<.input
|
||||
type="date"
|
||||
id={"cdf-#{field.id}-from"}
|
||||
name={"#{@custom_date_filter_prefix}#{field.id}_from"}
|
||||
label={gettext("From")}
|
||||
class="input input-sm input-bordered"
|
||||
aria-label={gettext("%{field} from", field: field.name)}
|
||||
value={custom_date_value_for_input(@date_filters, field.id, :from)}
|
||||
/>
|
||||
<.input
|
||||
type="date"
|
||||
id={"cdf-#{field.id}-to"}
|
||||
name={"#{@custom_date_filter_prefix}#{field.id}_to"}
|
||||
label={gettext("To")}
|
||||
class="input input-sm input-bordered"
|
||||
aria-label={gettext("%{field} to", field: field.name)}
|
||||
value={custom_date_value_for_input(@date_filters, field.id, :to)}
|
||||
/>
|
||||
</div>
|
||||
</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">
|
||||
|
|
@ -452,11 +646,13 @@ defmodule MvWeb.Components.MemberFilterComponent do
|
|||
)
|
||||
|
||||
custom_boolean_filters_parsed = parse_custom_boolean_filters(params)
|
||||
new_date_filters = DateFilter.from_params(params, socket.assigns.date_custom_fields)
|
||||
|
||||
dispatch_payment_filter_change(socket, payment_filter)
|
||||
dispatch_group_filter_changes(socket, group_filters_parsed)
|
||||
dispatch_fee_type_filter_changes(socket, fee_type_filters_parsed)
|
||||
dispatch_boolean_filter_changes(socket, custom_boolean_filters_parsed)
|
||||
dispatch_date_filters_change(socket, new_date_filters)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
|
@ -540,6 +736,12 @@ defmodule MvWeb.Components.MemberFilterComponent do
|
|||
end)
|
||||
end
|
||||
|
||||
defp dispatch_date_filters_change(socket, new_date_filters) do
|
||||
if new_date_filters != socket.assigns.date_filters do
|
||||
send(self(), {:date_filters_changed, new_date_filters})
|
||||
end
|
||||
end
|
||||
|
||||
# Get display label for button
|
||||
defp button_label(
|
||||
cycle_status_filter,
|
||||
|
|
@ -548,14 +750,16 @@ defmodule MvWeb.Components.MemberFilterComponent do
|
|||
fee_types,
|
||||
fee_type_filters,
|
||||
boolean_custom_fields,
|
||||
boolean_filters
|
||||
boolean_filters,
|
||||
date_filters
|
||||
) do
|
||||
active_count =
|
||||
count_active_filter_categories(
|
||||
cycle_status_filter,
|
||||
group_filters,
|
||||
fee_type_filters,
|
||||
boolean_filters
|
||||
boolean_filters,
|
||||
date_filters
|
||||
)
|
||||
|
||||
if active_count >= 2 do
|
||||
|
|
@ -576,6 +780,9 @@ defmodule MvWeb.Components.MemberFilterComponent do
|
|||
map_size(boolean_filters) > 0 ->
|
||||
boolean_filter_label(boolean_custom_fields, boolean_filters)
|
||||
|
||||
date_filters_active?(date_filters) ->
|
||||
gettext("Dates")
|
||||
|
||||
true ->
|
||||
gettext("Apply filters")
|
||||
end
|
||||
|
|
@ -586,17 +793,27 @@ defmodule MvWeb.Components.MemberFilterComponent do
|
|||
cycle_status_filter,
|
||||
group_filters,
|
||||
fee_type_filters,
|
||||
boolean_filters
|
||||
boolean_filters,
|
||||
date_filters
|
||||
) do
|
||||
[
|
||||
cycle_status_filter,
|
||||
map_size(group_filters) > 0,
|
||||
map_size(fee_type_filters) > 0,
|
||||
map_size(boolean_filters) > 0
|
||||
map_size(boolean_filters) > 0,
|
||||
date_filters_active?(date_filters)
|
||||
]
|
||||
|> Enum.count(& &1)
|
||||
end
|
||||
|
||||
# Date filter is "active" when its state differs from the default — i.e. the
|
||||
# user selected something other than active-only with no custom date bounds.
|
||||
defp date_filters_active?(date_filters) when is_map(date_filters) do
|
||||
date_filters != DateFilter.default()
|
||||
end
|
||||
|
||||
defp date_filters_active?(_), do: false
|
||||
|
||||
defp group_filters_label(_groups, group_filters) when map_size(group_filters) == 0,
|
||||
do: gettext("All")
|
||||
|
||||
|
|
@ -765,4 +982,35 @@ defmodule MvWeb.Components.MemberFilterComponent do
|
|||
"#{base_classes} btn-outline"
|
||||
end
|
||||
end
|
||||
|
||||
# --- Date filter helpers ----------------------------------------------
|
||||
|
||||
defp exit_mode(%{exit_date: %{mode: mode}}), do: mode
|
||||
defp exit_mode(_), do: :active_only
|
||||
|
||||
defp exit_mode_label_class(date_filters, expected) do
|
||||
base_classes = "join-item btn btn-sm"
|
||||
|
||||
if exit_mode(date_filters) == expected do
|
||||
"#{base_classes} btn-active"
|
||||
else
|
||||
"#{base_classes} btn"
|
||||
end
|
||||
end
|
||||
|
||||
defp date_value_for_input(date_filters, field, bound) do
|
||||
case date_filters do
|
||||
%{^field => %{^bound => %Date{} = d}} -> Date.to_iso8601(d)
|
||||
_ -> ""
|
||||
end
|
||||
end
|
||||
|
||||
defp custom_date_value_for_input(date_filters, field_id, bound) do
|
||||
key = to_string(field_id)
|
||||
|
||||
case Map.get(date_filters, key) do
|
||||
%{^bound => %Date{} = d} -> Date.to_iso8601(d)
|
||||
_ -> ""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@
|
|||
fee_type_filters={@fee_type_filters}
|
||||
boolean_custom_fields={@boolean_custom_fields}
|
||||
boolean_filters={@boolean_custom_field_filters}
|
||||
date_custom_fields={@date_custom_fields}
|
||||
date_filters={@date_filters}
|
||||
member_count={length(@members)}
|
||||
/>
|
||||
<.tooltip
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue