Rework member overview #553
4 changed files with 48 additions and 32 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule MvWeb.Components.BulkActionsDropdown do
|
defmodule MvWeb.Components.BulkActionsDropdown do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Single "Aktionen" dropdown bundling the four member bulk actions, flattened to
|
Single "Actions" dropdown bundling the four member bulk actions, flattened to
|
||||||
one level: open in email program (mailto), copy email addresses, export to CSV,
|
one level: open in email program (mailto), copy email addresses, export to CSV,
|
||||||
export to PDF.
|
export to PDF.
|
||||||
|
|
||||||
|
|
@ -10,10 +10,10 @@ defmodule MvWeb.Components.BulkActionsDropdown do
|
||||||
|
|
||||||
## Scope and trigger badge
|
## Scope and trigger badge
|
||||||
|
|
||||||
The trigger reads `Aktionen` followed by a scope badge: an emphasized
|
The trigger reads the `Actions` label followed by a scope badge: an emphasized
|
||||||
(`primary`) count `N` when `N` members are selected, and a muted (`neutral`)
|
(`primary`) count `N` when `N` members are selected, and a muted (`neutral`)
|
||||||
badge otherwise — `gefiltert` when a search term or filter narrows the list,
|
badge otherwise — `filtered` when a search term or filter narrows the list,
|
||||||
`alle` when nothing is selected and no search/filter is active. Only an actual
|
`all` when nothing is selected and no search/filter is active. Only an actual
|
||||||
selection is emphasized. The badge sits inside the shared `dropdown_menu/1`
|
selection is emphasized. The badge sits inside the shared `dropdown_menu/1`
|
||||||
trigger via its `trigger_badge` slot, matching the member-filter dropdown's
|
trigger via its `trigger_badge` slot, matching the member-filter dropdown's
|
||||||
count badge. The `scope`, `selected_count`, `mailto_bcc`, `recipient_count`
|
count badge. The `scope`, `selected_count`, `mailto_bcc`, `recipient_count`
|
||||||
|
|
@ -216,7 +216,7 @@ defmodule MvWeb.Components.BulkActionsDropdown do
|
||||||
gettext("Too many recipients for this function. Copy the addresses or export the list.")
|
gettext("Too many recipients for this function. Copy the addresses or export the list.")
|
||||||
end
|
end
|
||||||
|
|
||||||
# The trigger scope is shown as a badge after the "Aktionen" label. Only an
|
# The trigger scope is shown as a badge after the "Actions" label. Only an
|
||||||
# actual selection is emphasized (primary); both the "filtered" and "all"
|
# actual selection is emphasized (primary); both the "filtered" and "all"
|
||||||
# scopes are muted (neutral), since neither means members are selected.
|
# scopes are muted (neutral), since neither means members are selected.
|
||||||
defp scope_label(assigns) do
|
defp scope_label(assigns) do
|
||||||
|
|
|
||||||
|
|
@ -4,24 +4,40 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Full-text search across member profiles using PostgreSQL tsvector
|
- Full-text search across member profiles using PostgreSQL tsvector
|
||||||
- Sortable columns (name, email, address fields)
|
- DB-side filtering, sorting, and keyset pagination (infinite scroll)
|
||||||
- Bulk selection for future batch operations
|
- Sortable columns, including composite Name (last→first) and Address (city→postal→street)
|
||||||
- Real-time updates via LiveView
|
- Column manager and per-browser view settings (density, composite Member/Address, include-email)
|
||||||
- Bookmarkable URLs with query parameters
|
- Bulk selection spanning the whole filtered set (select-all / copy emails / export)
|
||||||
|
- Bookmarkable URLs: filter/sort/search/view state is URL-encoded
|
||||||
|
|
||||||
## URL Parameters
|
## URL Parameters
|
||||||
- `query` - Search query string for full-text search
|
All list state is URL-encoded so the view is bookmarkable and restored on reload:
|
||||||
- `sort_field` - Field to sort by (e.g., :first_name, :email, :join_date)
|
- `query` - full-text search string
|
||||||
- `sort_order` - Sort direction (:asc or :desc)
|
- `sort_field` / `sort_order` - sort column and direction (`asc` / `desc`)
|
||||||
|
- `fields` - visible-column selection
|
||||||
|
- `highlight` - member id to render as selected (return from the show page)
|
||||||
|
- group / fee-type / boolean / date custom-field filter params (see
|
||||||
|
`MvWeb.MemberLive.Index.FilterParams` and `DateFilter` for the encoding)
|
||||||
|
|
||||||
## Events
|
## Events (`handle_event`)
|
||||||
- `select_member` - Toggle individual member selection
|
- `select_member` / `select_all` - toggle individual / whole-filtered-set selection
|
||||||
- `select_all` - Toggle selection of all visible members
|
- `select_row_and_navigate` - open a member's show page
|
||||||
- `copy_emails` - Copy email addresses of the selected members, or of all/filtered members when nothing is selected
|
- `load_more` - fetch and append the next keyset page (infinite scroll)
|
||||||
|
- `sort` - change the active sort column/direction
|
||||||
|
- `toggle_cycle_view` - switch the payment-status view between current and last cycle
|
||||||
|
- `copy_emails` - copy emails of the selected members, or of the whole filtered set when none are selected
|
||||||
|
|
||||||
|
## Messages (`handle_info`, from the filter/search/view-settings components)
|
||||||
|
- `search_changed`, `view_setting_toggled`, `field_toggled`, `fields_selected`,
|
||||||
|
`fields_reset`, `reset_all_filters`, and the per-filter change messages
|
||||||
|
(`group_filter_changed`, `fee_type_filter_changed`, `boolean_filter_changed`,
|
||||||
|
`date_filters_changed`, `payment_filter_changed`)
|
||||||
|
|
||||||
## Implementation Notes
|
## Implementation Notes
|
||||||
- Search uses PostgreSQL full-text search (plainto_tsquery)
|
- Filtering/sorting/pagination run in PostgreSQL via Ash; the socket holds only
|
||||||
- Sort state is synced with URL for bookmarkability
|
the loaded keyset window, not the whole table
|
||||||
|
- List state is synced with the URL for bookmarkability; a signature guard
|
||||||
|
avoids redundant reloads
|
||||||
- Components communicate via `handle_info` for decoupling
|
- Components communicate via `handle_info` for decoupling
|
||||||
"""
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
|
|
@ -980,7 +996,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
|
|
||||||
# Flips the "include email" sub-toggle. Only meaningful while the composite
|
# Flips the "include email" sub-toggle. Only meaningful while the composite
|
||||||
# Member cell is on: it moves the email between an in-cell line (on) and a
|
# Member cell is on: it moves the email between an in-cell line (on) and a
|
||||||
# separate E-Mail column (off), so besides re-rendering the rows it recomputes
|
# separate email column (off), so besides re-rendering the rows it recomputes
|
||||||
# which columns the manager offers and their visibility.
|
# which columns the manager offers and their visibility.
|
||||||
defp toggle_view_setting(socket, :member_include_email) do
|
defp toggle_view_setting(socket, :member_include_email) do
|
||||||
new_value = not socket.assigns.view_settings.member_include_email
|
new_value = not socket.assigns.view_settings.member_include_email
|
||||||
|
|
@ -995,10 +1011,10 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Flips the compact Member field. Besides re-rendering the rows, this changes
|
# Flips the compact Member field. Besides re-rendering the rows, this changes
|
||||||
# which columns the manager offers (composite "Name" vs. Vorname/Nachname/E-Mail,
|
# which columns the manager offers (composite "Name" vs. first-name/last-name/email,
|
||||||
# §7b). The email visibility carries over between the two representations,
|
# §7b). The email visibility carries over between the two representations,
|
||||||
# analogously to the "include email" sub-toggle: composite + include-email ⇄ the
|
# analogously to the "include email" sub-toggle: composite + include-email ⇄ the
|
||||||
# E-Mail column being visible.
|
# email column being visible.
|
||||||
defp toggle_view_setting(socket, :compact_member) do
|
defp toggle_view_setting(socket, :compact_member) do
|
||||||
new_value = not socket.assigns.view_settings.compact_member
|
new_value = not socket.assigns.view_settings.compact_member
|
||||||
email_visible? = email_currently_visible?(socket)
|
email_visible? = email_currently_visible?(socket)
|
||||||
|
|
@ -1013,8 +1029,8 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
|> push_field_selection_url()
|
|> push_field_selection_url()
|
||||||
end
|
end
|
||||||
|
|
||||||
# Flips the compact Address field: offers the composite "Adresse" vs. the
|
# Flips the compact Address field: offers the composite "Address" vs. the
|
||||||
# separate Straße/Hausnummer/PLZ/Ort columns, making the newly relevant columns
|
# separate street/house-number/postal-code/city columns, making the newly relevant columns
|
||||||
# visible and recomputing the offered set + visibility for the new mode.
|
# visible and recomputing the offered set + visibility for the new mode.
|
||||||
defp toggle_view_setting(socket, :compact_address) do
|
defp toggle_view_setting(socket, :compact_address) do
|
||||||
new_value = not socket.assigns.view_settings.compact_address
|
new_value = not socket.assigns.view_settings.compact_address
|
||||||
|
|
@ -1031,7 +1047,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Whether the member email is currently surfaced: while the composite is on,
|
# Whether the member email is currently surfaced: while the composite is on,
|
||||||
# either folded into the cell (include_email) or as a separate E-Mail column;
|
# either folded into the cell (include_email) or as a separate email column;
|
||||||
# while it is off, as its own column. Read before the flip, so `compact_member`
|
# while it is off, as its own column. Read before the flip, so `compact_member`
|
||||||
# still holds the previous mode.
|
# still holds the previous mode.
|
||||||
defp email_currently_visible?(socket) do
|
defp email_currently_visible?(socket) do
|
||||||
|
|
@ -1063,8 +1079,8 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
|
|
||||||
# Makes the now-relevant Member columns visible for the new mode. Turning the
|
# Makes the now-relevant Member columns visible for the new mode. Turning the
|
||||||
# composite on surfaces "Name" (with the email either folded via include_email
|
# composite on surfaces "Name" (with the email either folded via include_email
|
||||||
# or kept as a separate column); turning it off surfaces Vorname/Nachname and
|
# or kept as a separate column); turning it off surfaces first-name/last-name and
|
||||||
# carries the prior email visibility onto the E-Mail column.
|
# carries the prior email visibility onto the email column.
|
||||||
defp apply_member_toggle(socket, true = _new_compact, email_visible?) do
|
defp apply_member_toggle(socket, true = _new_compact, email_visible?) do
|
||||||
selection =
|
selection =
|
||||||
socket.assigns.user_field_selection
|
socket.assigns.user_field_selection
|
||||||
|
|
@ -1910,7 +1926,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
@doc """
|
@doc """
|
||||||
Returns true when the member list is restricted by a non-empty search term or
|
Returns true when the member list is restricted by a non-empty search term or
|
||||||
any active filter (cycle status, group, fee type, boolean custom field, or a
|
any active filter (cycle status, group, fee type, boolean custom field, or a
|
||||||
date filter differing from the default). Drives the "(gefiltert)" vs "(alle)"
|
date filter differing from the default). Drives the "filtered" vs "all"
|
||||||
trigger label and reads only assigns — no DB access.
|
trigger label and reads only assigns — no DB access.
|
||||||
"""
|
"""
|
||||||
def filters_active?(assigns) do
|
def filters_active?(assigns) do
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do
|
||||||
defp apply_name_default(set, true = _compact, true = _include_email), do: set
|
defp apply_name_default(set, true = _compact, true = _include_email), do: set
|
||||||
|
|
||||||
# Compact Member cell without folded email: the composite plus a separate
|
# Compact Member cell without folded email: the composite plus a separate
|
||||||
# default-visible E-Mail column.
|
# default-visible email column.
|
||||||
defp apply_name_default(set, true = _compact, false = _include_email),
|
defp apply_name_default(set, true = _compact, false = _include_email),
|
||||||
do: MapSet.put(set, :email)
|
do: MapSet.put(set, :email)
|
||||||
|
|
||||||
|
|
@ -111,7 +111,7 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do
|
||||||
3. the address group — the composite `:address` when compact, otherwise its
|
3. the address group — the composite `:address` when compact, otherwise its
|
||||||
constituents `:street`, `:house_number`, `:postal_code`, `:city`.
|
constituents `:street`, `:house_number`, `:postal_code`, `:city`.
|
||||||
|
|
||||||
Everything else follows in the configured Datenfelder order. This keeps the
|
Everything else follows in the configured data-fields order. This keeps the
|
||||||
name group ahead of the address group even when the name group is expanded
|
name group ahead of the address group even when the name group is expanded
|
||||||
into `:first_name`/`:last_name` while the address stays compact.
|
into `:first_name`/`:last_name` while the address stays compact.
|
||||||
"""
|
"""
|
||||||
|
|
@ -145,7 +145,7 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do
|
||||||
address_variant_hidden?(field, compact_address)
|
address_variant_hidden?(field, compact_address)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Compact hides Vorname/Nachname (folded into the composite). The email is
|
# Compact hides first-name/last-name (folded into the composite). The email is
|
||||||
# hidden only when it is folded into the cell (compact + include_email); with
|
# hidden only when it is folded into the cell (compact + include_email); with
|
||||||
# include_email off it stays offered as a separate column. Non-compact hides the
|
# include_email off it stays offered as a separate column. Non-compact hides the
|
||||||
# composite in favour of the constituents.
|
# composite in favour of the constituents.
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@ defmodule MvWeb.MemberLive.Index.ViewSettings do
|
||||||
|
|
||||||
* `:density` — `:compact` or `:comfortable` (the row-spacing / compact mode)
|
* `:density` — `:compact` or `:comfortable` (the row-spacing / compact mode)
|
||||||
* `:compact_member` — composite "Member" cell (last+first name) vs separate
|
* `:compact_member` — composite "Member" cell (last+first name) vs separate
|
||||||
Vorname/Nachname columns
|
first-name/last-name columns
|
||||||
* `:member_include_email` — whether the email is surfaced (email line in the
|
* `:member_include_email` — whether the email is surfaced (email line in the
|
||||||
composite cell, or a separate Email column when the member field is split)
|
composite cell, or a separate Email column when the member field is split)
|
||||||
* `:compact_address` — composite Address cell (street / postal+city) vs
|
* `:compact_address` — composite Address cell (street / postal+city) vs
|
||||||
separate Straße/PLZ/Ort columns
|
separate street/postal-code/city columns
|
||||||
|
|
||||||
Persistence is per browser (not per account), resolved in priority order from
|
Persistence is per browser (not per account), resolved in priority order from
|
||||||
the LiveView connect params, the session, the request cookie, and finally the
|
the LiveView connect params, the session, the request cookie, and finally the
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue