feat(member): condense the overview into composite name and address cells
This commit is contained in:
parent
b79d7ac9ea
commit
af2cc2e0d4
17 changed files with 716 additions and 93 deletions
|
|
@ -953,6 +953,11 @@ defmodule MvWeb.CoreComponents do
|
|||
doc:
|
||||
"when true, first header/body column gets sticky left positioning to keep selection controls visible"
|
||||
|
||||
attr :sticky_second_col, :boolean,
|
||||
default: false,
|
||||
doc:
|
||||
"when true, the second column is also pinned left (e.g. an identifier column kept visible while scrolling horizontally)"
|
||||
|
||||
attr :viewport_bottom, :any,
|
||||
default: nil,
|
||||
doc:
|
||||
|
|
@ -1018,7 +1023,9 @@ defmodule MvWeb.CoreComponents do
|
|||
:for={{col, col_idx} <- Enum.with_index(@col)}
|
||||
class={[
|
||||
table_th_class(col, @sticky_header),
|
||||
@sticky_first_col && col_idx == 0 && "sticky left-0 z-30 bg-base-100"
|
||||
@sticky_first_col && col_idx == 0 &&
|
||||
"sticky-first-col-th sticky left-0 z-30 bg-base-100",
|
||||
@sticky_second_col && col_idx == 1 && "sticky left-12 z-30 bg-base-100"
|
||||
]}
|
||||
aria-sort={table_th_aria_sort(col, @sort_field, @sort_order)}
|
||||
>
|
||||
|
|
@ -1079,6 +1086,13 @@ defmodule MvWeb.CoreComponents do
|
|||
classes
|
||||
end
|
||||
|
||||
classes =
|
||||
if @sticky_second_col && col_idx == 1 do
|
||||
["sticky left-12 z-20 bg-base-100" | classes]
|
||||
else
|
||||
classes
|
||||
end
|
||||
|
||||
classes =
|
||||
if col_class == nil || (col_class && !String.contains?(col_class, "text-center")) do
|
||||
["truncate" | classes]
|
||||
|
|
|
|||
|
|
@ -49,6 +49,17 @@ defmodule MvWeb.Components.SearchBarComponent do
|
|||
phx-target={@myself}
|
||||
phx-debounce="300"
|
||||
/>
|
||||
<button
|
||||
:if={@query not in [nil, ""]}
|
||||
type="button"
|
||||
phx-click="clear_search"
|
||||
phx-target={@myself}
|
||||
data-testid="search-clear"
|
||||
aria-label={gettext("Clear search")}
|
||||
class="opacity-50 hover:opacity-100 cursor-pointer"
|
||||
>
|
||||
<.icon name="hero-x-mark" class="h-[1em]" />
|
||||
</button>
|
||||
</label>
|
||||
</form>
|
||||
"""
|
||||
|
|
@ -61,4 +72,10 @@ defmodule MvWeb.Components.SearchBarComponent do
|
|||
send(self(), {:search_changed, q})
|
||||
{:noreply, assign(socket, :query, q)}
|
||||
end
|
||||
|
||||
# Clears the query and resets the result set to the unfiltered list (§1.3).
|
||||
def handle_event("clear_search", _params, socket) do
|
||||
send(self(), {:search_changed, ""})
|
||||
{:noreply, assign(socket, :query, "")}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -149,6 +149,48 @@
|
|||
role="checkbox"
|
||||
/>
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
:if={:name in @member_fields_visible}
|
||||
label={gettext("Name")}
|
||||
>
|
||||
<% full_name =
|
||||
[member.first_name, member.last_name]
|
||||
|> Enum.reject(&(&1 in [nil, ""]))
|
||||
|> Enum.join(" ") %>
|
||||
<div data-testid="member-name">
|
||||
<div class="font-medium truncate" title={full_name}>
|
||||
{full_name}
|
||||
</div>
|
||||
<div
|
||||
:if={member.email not in [nil, ""]}
|
||||
class="opacity-70 truncate"
|
||||
title={member.email}
|
||||
>
|
||||
{member.email}
|
||||
</div>
|
||||
</div>
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
:if={:address in @member_fields_visible}
|
||||
label={gettext("Address")}
|
||||
>
|
||||
<% line1 =
|
||||
[member.street, member.house_number]
|
||||
|> Enum.reject(&(&1 in [nil, ""]))
|
||||
|> Enum.join(" ") %>
|
||||
<% line2 =
|
||||
[member.postal_code, member.city]
|
||||
|> Enum.reject(&(&1 in [nil, ""]))
|
||||
|> Enum.join(" ") %>
|
||||
<.maybe_value value={line1 <> line2} empty_sr_text={gettext("No address")}>
|
||||
<div data-testid="member-address">
|
||||
<div class="truncate" title={line1}>{line1}</div>
|
||||
<div class="opacity-70 truncate" title={line2}>{line2}</div>
|
||||
</div>
|
||||
</.maybe_value>
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
:if={:first_name in @member_fields_visible}
|
||||
|
|
@ -280,7 +322,9 @@
|
|||
"""
|
||||
}
|
||||
>
|
||||
{member.city}
|
||||
<.maybe_value value={member.city} empty_sr_text={gettext("Not specified")}>
|
||||
{member.city}
|
||||
</.maybe_value>
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
|
|
@ -298,7 +342,9 @@
|
|||
"""
|
||||
}
|
||||
>
|
||||
{member.street}
|
||||
<.maybe_value value={member.street} empty_sr_text={gettext("Not specified")}>
|
||||
{member.street}
|
||||
</.maybe_value>
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
|
|
@ -316,7 +362,9 @@
|
|||
"""
|
||||
}
|
||||
>
|
||||
{member.house_number}
|
||||
<.maybe_value value={member.house_number} empty_sr_text={gettext("Not specified")}>
|
||||
{member.house_number}
|
||||
</.maybe_value>
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
|
|
@ -334,7 +382,9 @@
|
|||
"""
|
||||
}
|
||||
>
|
||||
{member.postal_code}
|
||||
<.maybe_value value={member.postal_code} empty_sr_text={gettext("Not specified")}>
|
||||
{member.postal_code}
|
||||
</.maybe_value>
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
|
|
@ -407,15 +457,17 @@
|
|||
}
|
||||
>
|
||||
<.maybe_value value={member.groups} empty_sr_text={gettext("No group assignment")}>
|
||||
<%= for group <- (member.groups || []) do %>
|
||||
<.badge
|
||||
variant="primary"
|
||||
style="outline"
|
||||
aria-label={gettext("Member of group %{name}", name: group.name)}
|
||||
>
|
||||
{group.name}
|
||||
</.badge>
|
||||
<% end %>
|
||||
<div class="flex flex-wrap gap-1 w-max">
|
||||
<%= for group <- (member.groups || []) do %>
|
||||
<.badge
|
||||
variant="primary"
|
||||
style="outline"
|
||||
aria-label={gettext("Member of group %{name}", name: group.name)}
|
||||
>
|
||||
{group.name}
|
||||
</.badge>
|
||||
<% end %>
|
||||
</div>
|
||||
</.maybe_value>
|
||||
</:col>
|
||||
<:action :let={member}>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,28 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do
|
|||
|
||||
# Single UI key for "Membership Fee Status"; only this appears in the dropdown.
|
||||
# Groups and membership_fee_type are also pseudo fields (not in member_fields(), displayed in the table).
|
||||
@pseudo_member_fields [:membership_fee_status, :membership_fee_type, :groups]
|
||||
# :name and :address are composite display-only columns (Name = name + email,
|
||||
# Address = street/house number over postal code/city).
|
||||
@pseudo_member_fields [:membership_fee_status, :membership_fee_type, :groups, :name, :address]
|
||||
|
||||
# Curated default-visible column set (§1.2): the columns shown on first visit
|
||||
# when there is no persisted selection and no global override. Everything else
|
||||
# (the individual name/address sub-fields, notes, dates other than join date)
|
||||
# defaults to hidden so the table fits common desktop widths.
|
||||
@default_visible_fields MapSet.new([
|
||||
:name,
|
||||
:address,
|
||||
:membership_fee_type,
|
||||
:membership_fee_status,
|
||||
:groups,
|
||||
:join_date
|
||||
])
|
||||
|
||||
@doc """
|
||||
The curated default-visible columns (member-field atoms) for a first visit.
|
||||
"""
|
||||
@spec default_visible_fields() :: [atom()]
|
||||
def default_visible_fields, do: MapSet.to_list(@default_visible_fields)
|
||||
|
||||
# Export/API may accept this as alias; must not appear in the UI options list.
|
||||
@export_only_alias :payment_status
|
||||
|
|
@ -276,13 +297,13 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do
|
|||
domain_map =
|
||||
Enum.reduce(domain_fields, %{}, fn field, acc ->
|
||||
field_string = Atom.to_string(field)
|
||||
default_visibility = if field == :exit_date, do: false, else: true
|
||||
default_visibility = MapSet.member?(@default_visible_fields, field)
|
||||
show_in_overview = Map.get(visibility_config, field, default_visibility)
|
||||
Map.put(acc, field_string, show_in_overview)
|
||||
end)
|
||||
|
||||
Enum.reduce(@pseudo_member_fields, domain_map, fn field, acc ->
|
||||
Map.put(acc, Atom.to_string(field), true)
|
||||
Map.put(acc, Atom.to_string(field), MapSet.member?(@default_visible_fields, field))
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ defmodule MvWeb.Translations.MemberFields do
|
|||
def label(:membership_fee_status), do: gettext("Membership Fee Status")
|
||||
def label(:membership_fee_type), do: gettext("Fee Type")
|
||||
def label(:groups), do: gettext("Groups")
|
||||
def label(:name), do: gettext("Name")
|
||||
def label(:address), do: gettext("Address")
|
||||
|
||||
# Fallback for unknown fields
|
||||
def label(field) do
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue