refactor(overview): use English identifier as_of_date for point-in-time filter
This commit is contained in:
parent
ce877463d6
commit
0acd12360f
11 changed files with 113 additions and 112 deletions
|
|
@ -49,6 +49,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
alias Mv.MembershipFees
|
||||
alias Mv.MembershipFees.MembershipFeeType
|
||||
alias MvWeb.Helpers.DateFormatter
|
||||
alias MvWeb.MemberLive.Index.AsOfDate
|
||||
alias MvWeb.MemberLive.Index.CustomFieldValueLookup
|
||||
alias MvWeb.MemberLive.Index.DateFilter
|
||||
alias MvWeb.MemberLive.Index.ExportPayload
|
||||
|
|
@ -59,7 +60,6 @@ defmodule MvWeb.MemberLive.Index do
|
|||
alias MvWeb.MemberLive.Index.MembershipFeeStatus
|
||||
alias MvWeb.MemberLive.Index.OverviewQuery
|
||||
alias MvWeb.MemberLive.Index.PaymentAging
|
||||
alias MvWeb.MemberLive.Index.Stichtag
|
||||
alias MvWeb.MemberLive.Index.ViewSettings
|
||||
|
||||
require Ash.Query
|
||||
|
|
@ -179,7 +179,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
|> assign(:payment_filter, nil)
|
||||
|> assign(:payment_period, PaymentAging.default_period())
|
||||
|> assign(:suspended, false)
|
||||
|> assign(:stichtag, nil)
|
||||
|> assign(:as_of_date, nil)
|
||||
|> assign(:group_filters, %{})
|
||||
|> assign(:groups, groups)
|
||||
|> assign(:fee_type_filters, %{})
|
||||
|
|
@ -473,7 +473,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
payment_filter: nil,
|
||||
payment_period: PaymentAging.default_period(),
|
||||
suspended: false,
|
||||
stichtag: nil,
|
||||
as_of_date: nil,
|
||||
group_filters: %{},
|
||||
fee_type_filters: %{},
|
||||
boolean_filters: %{},
|
||||
|
|
@ -504,16 +504,16 @@ defmodule MvWeb.MemberLive.Index do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:stichtag_changed, stichtag}, socket) do
|
||||
def handle_info({:as_of_date_changed, as_of_date}, socket) do
|
||||
socket =
|
||||
socket
|
||||
|> assign(:stichtag, stichtag)
|
||||
|> assign(:as_of_date, as_of_date)
|
||||
|> load_members()
|
||||
|> scroll_list_to_top()
|
||||
|> update_selection_assigns()
|
||||
|
||||
query_params =
|
||||
build_query_params(opts_for_query_params(socket, %{stichtag: stichtag}))
|
||||
build_query_params(opts_for_query_params(socket, %{as_of_date: as_of_date}))
|
||||
|> maybe_add_field_selection(
|
||||
socket.assigns[:user_field_selection],
|
||||
socket.assigns[:fields_in_url?] || false
|
||||
|
|
@ -638,7 +638,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
|> assign(:boolean_custom_field_filters, Map.get(opts, :boolean_filters, %{}))
|
||||
|> assign(:date_filters, Map.get(opts, :date_filters, DateFilter.default()))
|
||||
|> assign(:suspended, Map.get(opts, :suspended, false))
|
||||
|> assign(:stichtag, Map.get(opts, :stichtag))
|
||||
|> assign(:as_of_date, Map.get(opts, :as_of_date))
|
||||
|> load_members()
|
||||
|> scroll_list_to_top()
|
||||
|> update_selection_assigns()
|
||||
|
|
@ -727,7 +727,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
|> maybe_update_payment_filter(params)
|
||||
|> maybe_update_payment_period(params)
|
||||
|> maybe_update_suspended(params)
|
||||
|> maybe_update_stichtag(params)
|
||||
|> maybe_update_as_of_date(params)
|
||||
|> maybe_update_group_filters(params)
|
||||
|> maybe_update_fee_type_filters(params)
|
||||
|> maybe_update_boolean_filters(params)
|
||||
|
|
@ -778,7 +778,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
socket.assigns.payment_filter,
|
||||
socket.assigns.payment_period,
|
||||
socket.assigns[:suspended],
|
||||
socket.assigns[:stichtag],
|
||||
socket.assigns[:as_of_date],
|
||||
socket.assigns[:group_filters],
|
||||
socket.assigns[:fee_type_filters],
|
||||
socket.assigns.boolean_custom_field_filters,
|
||||
|
|
@ -934,17 +934,17 @@ defmodule MvWeb.MemberLive.Index do
|
|||
base_params = add_group_filters(base_params, opts.group_filters || %{})
|
||||
base_params = add_fee_type_filters(base_params, opts.fee_type_filters || %{})
|
||||
base_params = add_payment_params(base_params, opts.payment_filter, opts.payment_period)
|
||||
base_params = add_status_params(base_params, opts[:suspended], opts[:stichtag])
|
||||
base_params = add_status_params(base_params, opts[:suspended], opts[:as_of_date])
|
||||
base_params = add_boolean_filters(base_params, opts.boolean_filters || %{})
|
||||
add_date_filters(base_params, opts.date_filters)
|
||||
end
|
||||
|
||||
# Suspended-status flag and Stichtag date serialize additively into the flat
|
||||
# URL contract (suspended=1 / stichtag=<iso>), omitted when inactive (§2.1).
|
||||
defp add_status_params(params, suspended, stichtag) do
|
||||
# Suspended-status flag and as-of date serialize additively into the flat
|
||||
# URL contract (suspended=1 / as_of_date=<iso>), omitted when inactive (§2.1).
|
||||
defp add_status_params(params, suspended, as_of_date) do
|
||||
params
|
||||
|> Map.merge(PaymentAging.suspended_to_params(suspended))
|
||||
|> Map.merge(Stichtag.to_params(stichtag))
|
||||
|> Map.merge(AsOfDate.to_params(as_of_date))
|
||||
end
|
||||
|
||||
# Period-scoped payment model (§3.3): the payment-count filter and the active
|
||||
|
|
@ -967,7 +967,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
payment_filter: socket.assigns.payment_filter,
|
||||
payment_period: socket.assigns.payment_period,
|
||||
suspended: socket.assigns[:suspended] || false,
|
||||
stichtag: socket.assigns[:stichtag],
|
||||
as_of_date: socket.assigns[:as_of_date],
|
||||
group_filters: socket.assigns[:group_filters] || %{},
|
||||
boolean_filters: socket.assigns.boolean_custom_field_filters || %{},
|
||||
fee_type_filters: socket.assigns[:fee_type_filters] || %{},
|
||||
|
|
@ -1429,7 +1429,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
payment_filter: socket.assigns.payment_filter,
|
||||
payment_period: socket.assigns.payment_period,
|
||||
suspended: socket.assigns[:suspended] || false,
|
||||
stichtag: socket.assigns[:stichtag],
|
||||
as_of_date: socket.assigns[:as_of_date],
|
||||
sort_field: socket.assigns.sort_field,
|
||||
sort_order: socket.assigns.sort_order,
|
||||
custom_fields: socket.assigns.all_custom_fields
|
||||
|
|
@ -1644,14 +1644,14 @@ defmodule MvWeb.MemberLive.Index do
|
|||
defp maybe_update_payment_period(socket, params),
|
||||
do: assign(socket, :payment_period, PaymentAging.parse_period(params))
|
||||
|
||||
# Suspended-status filter (§1.23) and point-in-time Stichtag (§1.24/§2.6):
|
||||
# Suspended-status filter (§1.23) and point-in-time as-of date (§1.24/§2.6):
|
||||
# both decoded straight from the URL so `handle_params` remains the single
|
||||
# source of truth. Absent params fall back to no filter.
|
||||
defp maybe_update_suspended(socket, params),
|
||||
do: assign(socket, :suspended, PaymentAging.parse_suspended(params))
|
||||
|
||||
defp maybe_update_stichtag(socket, params),
|
||||
do: assign(socket, :stichtag, Stichtag.parse(params))
|
||||
defp maybe_update_as_of_date(socket, params),
|
||||
do: assign(socket, :as_of_date, AsOfDate.parse(params))
|
||||
|
||||
defp maybe_update_group_filters(socket, params) when is_map(params) do
|
||||
prefix = @group_filter_prefix
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@
|
|||
payment_filter={@payment_filter}
|
||||
payment_period={@payment_period}
|
||||
suspended={@suspended}
|
||||
stichtag={@stichtag}
|
||||
as_of_date={@as_of_date}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
37
lib/mv_web/live/member_live/index/as_of_date.ex
Normal file
37
lib/mv_web/live/member_live/index/as_of_date.ex
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
defmodule MvWeb.MemberLive.Index.AsOfDate do
|
||||
@moduledoc """
|
||||
URL codec for the point-in-time "active on X" membership filter (§1.24/§2.6).
|
||||
|
||||
The as-of date is a single ISO-8601 date X carried in the flat URL contract
|
||||
under the `as_of_date` key. When set, the overview keeps members who were
|
||||
active on X (`join_date <= X and (exit_date is nil or exit_date > X)`); the
|
||||
predicate itself lives in `OverviewQuery`. Absent or malformed values decode
|
||||
to nil (no filter), and nil encodes to the empty map so a fresh URL stays
|
||||
canonical.
|
||||
"""
|
||||
|
||||
@as_of_date_param Mv.Constants.as_of_date_param()
|
||||
|
||||
@doc """
|
||||
Decodes the as-of date from a params map. Missing or malformed ISO-8601
|
||||
values yield nil.
|
||||
"""
|
||||
@spec parse(map()) :: Date.t() | nil
|
||||
def parse(params) when is_map(params), do: parse_date(Map.get(params, @as_of_date_param))
|
||||
|
||||
@doc """
|
||||
Encodes an as-of date into URL params. nil yields the empty map.
|
||||
"""
|
||||
@spec to_params(Date.t() | nil) :: %{optional(String.t()) => String.t()}
|
||||
def to_params(%Date{} = date), do: %{@as_of_date_param => Date.to_iso8601(date)}
|
||||
def to_params(_), do: %{}
|
||||
|
||||
defp parse_date(value) when is_binary(value) do
|
||||
case Date.from_iso8601(String.trim(value)) do
|
||||
{:ok, date} -> date
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_date(_), do: nil
|
||||
end
|
||||
|
|
@ -46,7 +46,7 @@ defmodule MvWeb.MemberLive.Index.OverviewQuery do
|
|||
opts[:boolean_custom_fields]
|
||||
)
|
||||
|> apply_date_filters(opts[:date_filters])
|
||||
|> apply_stichtag_filter(opts[:stichtag])
|
||||
|> apply_as_of_date_filter(opts[:as_of_date])
|
||||
|> apply_custom_date_filters(opts[:date_filters], opts[:date_custom_fields])
|
||||
|> apply_payment_filter(opts[:payment_filter], payment_period(opts))
|
||||
|> apply_suspended_filter(opts[:suspended], payment_period(opts))
|
||||
|
|
@ -285,20 +285,20 @@ defmodule MvWeb.MemberLive.Index.OverviewQuery do
|
|||
defp apply_custom_date_filters(query, _filters, _custom_fields), do: query
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Point-in-time membership ("Stichtag"): a member counts as a member on the
|
||||
# Point-in-time membership (as-of date): a member counts as a member on the
|
||||
# reference date X iff it had joined by X and had not yet left as of X
|
||||
# (§1.24/§2.6). "Left as of X" means a non-nil exit_date at or before X, so an
|
||||
# exit_date strictly after X (or nil) still counts as a member on X.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
defp apply_stichtag_filter(query, %Date{} = x) do
|
||||
defp apply_as_of_date_filter(query, %Date{} = x) do
|
||||
Ash.Query.filter(
|
||||
query,
|
||||
expr(join_date <= ^x and (is_nil(exit_date) or exit_date > ^x))
|
||||
)
|
||||
end
|
||||
|
||||
defp apply_stichtag_filter(query, _), do: query
|
||||
defp apply_as_of_date_filter(query, _), do: query
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sort (always append the unique id tie-breaker for keyset stability)
|
||||
|
|
@ -327,7 +327,7 @@ defmodule MvWeb.MemberLive.Index.OverviewQuery do
|
|||
])
|
||||
end
|
||||
|
||||
# "Beiträge" (payment) column: sort DB-side by the period-scoped unpaid-cycle
|
||||
# Fees (payment) column: sort DB-side by the period-scoped unpaid-cycle
|
||||
# count (§1.27). The count calculation takes the active period as arguments so
|
||||
# the sort key matches the displayed value; the id tie-breaker keeps keyset
|
||||
# pages stable across equal counts.
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
defmodule MvWeb.MemberLive.Index.Stichtag do
|
||||
@moduledoc """
|
||||
URL codec for the point-in-time membership ("Stichtag") filter (§1.24/§2.6).
|
||||
|
||||
The Stichtag is a single ISO-8601 date X carried in the flat URL contract
|
||||
under the `stichtag` key. When set, the overview keeps members who were active
|
||||
on X (`join_date <= X and (exit_date is nil or exit_date > X)`); the predicate
|
||||
itself lives in `OverviewQuery`. Absent or malformed values decode to nil (no
|
||||
filter), and nil encodes to the empty map so a fresh URL stays canonical.
|
||||
"""
|
||||
|
||||
@stichtag_param Mv.Constants.stichtag_param()
|
||||
|
||||
@doc """
|
||||
Decodes the Stichtag date from a params map. Missing or malformed ISO-8601
|
||||
values yield nil.
|
||||
"""
|
||||
@spec parse(map()) :: Date.t() | nil
|
||||
def parse(params) when is_map(params), do: parse_date(Map.get(params, @stichtag_param))
|
||||
|
||||
@doc """
|
||||
Encodes a Stichtag date into URL params. nil yields the empty map.
|
||||
"""
|
||||
@spec to_params(Date.t() | nil) :: %{optional(String.t()) => String.t()}
|
||||
def to_params(%Date{} = date), do: %{@stichtag_param => Date.to_iso8601(date)}
|
||||
def to_params(_), do: %{}
|
||||
|
||||
defp parse_date(value) when is_binary(value) do
|
||||
case Date.from_iso8601(String.trim(value)) do
|
||||
{:ok, date} -> date
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_date(_), do: nil
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue