Packages

phoenix_kit

2.22.21
2.62.2 2.62.1 2.62.0 2.61.0 2.60.3 2.60.2 2.60.1 2.60.0 2.59.0 2.58.0 2.57.1 2.57.0 2.56.1 2.56.0 2.55.1 2.55.0 2.54.2 2.54.1 2.54.0 2.53.0 2.52.2 2.52.1 2.52.0 2.51.0 2.50.0 2.49.1 2.49.0 2.48.0 2.47.0 2.46.0 2.45.0 2.44.0 2.43.1 2.43.0 2.42.1 2.42.0 2.41.6 2.41.4 2.41.3 2.41.2 2.41.1 2.41.0 2.40.1 2.40.0 2.39.0 2.38.1 2.38.0 2.37.5 2.37.4 2.37.3 2.37.2 2.37.1 2.37.0 2.36.1 2.36.0 2.35.0 2.34.0 2.33.0 2.32.1 2.32.0 2.31.1 2.31.0 2.30.0 2.29.1 2.29.0 2.28.2 2.28.1 2.28.0 2.27.2 2.27.1 2.27.0 2.26.1 2.26.0 2.25.0 2.24.0 2.23.3 2.23.2 2.23.1 2.23.0 2.22.24 2.22.23 2.22.22 2.22.21 2.22.20 2.22.19 2.22.18 2.22.17 2.22.16 2.22.15 2.22.14 2.22.13 2.22.12 2.22.11 2.22.10 2.22.9 2.22.8 2.22.7 2.22.6 2.22.5 2.22.4 2.22.3 2.22.2 2.22.1 2.22.0 2.21.5 2.21.4 2.21.3 2.21.2 2.21.1 2.21.0 2.20.0 2.19.0 2.18.1 2.18.0 2.17.0 2.16.0 2.15.1 2.15.0 2.14.2 2.14.1 2.14.0 2.13.19 2.13.18 2.13.17 2.13.16 2.13.15 2.13.13 2.13.12 2.13.11 2.13.10 2.13.9 2.13.8 2.13.7 2.13.6 2.13.5 2.13.4 2.13.3 2.13.2 2.13.1 2.13.0 2.12.1 2.12.0 2.11.0 2.10.0 2.9.0 2.8.1 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.0 2.0.1 2.0.0 1.7.236 1.7.235 1.7.234 1.7.233 1.7.232 1.7.231 1.7.230 1.7.229 1.7.228 1.7.227 1.7.226 1.7.225 1.7.224 1.7.223 1.7.222 1.7.221 1.7.220 1.7.219 1.7.218 1.7.217 1.7.216 1.7.215 1.7.214 1.7.213 1.7.212 1.7.211 1.7.210 1.7.209 1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit_web live users users.html.heex
Raw

lib/phoenix_kit_web/live/users/users.html.heex

<PhoenixKitWeb.Components.LayoutWrapper.app_layout
  socket={@socket}
  flash={@flash}
  phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
  page_title={@page_title}
  page_subtitle={gettext("Manage user accounts and roles")}
  current_path={@url_path}
  project_title={@project_title}
  current_locale={@current_locale}
>
  <div class="flex flex-col w-full p-4">
    <%!-- Role Management Modal --%>
    <%= if @show_role_modal && @managing_user do %>
      <div class="modal modal-open">
        <div class="modal-box max-w-2xl">
          <h3 class="font-bold text-xl mb-4">
            Manage Roles for {@managing_user.email}
          </h3>

          <form phx-submit="sync_user_roles" class="space-y-4">
            <div class="space-y-2">
              <%= for role <- @all_roles do %>
                <% owner_role? = role.name == PhoenixKit.Users.Role.system_roles().owner %>
                <div class="fieldset">
                  <%!-- DO NOT convert to <.checkbox>: the submitted VALUE is the
                       payload here. `sync_user_roles` reads Map.values(params["roles"])
                       and the context diffs that list against the user's current roles,
                       so a component that hardcodes "true"/"false" makes every held role
                       look absent and strips them all. Tests do not catch it —
                       render_submit injects params and never serializes the DOM. --%>
                  <label
                    class={[
                      "label justify-start gap-3",
                      owner_role? && "cursor-not-allowed opacity-70"
                    ]}
                    title={
                      if owner_role?, do: gettext("Owner role cannot be modified"), else: nil
                    }
                  >
                    <input
                      type="checkbox"
                      name={"roles[#{role.name}]"}
                      value={role.name}
                      class="checkbox"
                      checked={role.name in @user_roles}
                      disabled={owner_role?}
                    />
                    <div class="flex flex-wrap items-center gap-2">
                      <.role_badge role={role} size={:sm} />
                      <%= if role.is_system_role do %>
                        <span class="badge badge-outline badge-xs h-auto">
                          {gettext("System")}
                        </span>
                      <% end %>
                      <%= if role.description do %>
                        <span class="text-sm text-base-content/70">- {role.description}</span>
                      <% end %>
                    </div>
                  </label>
                </div>
              <% end %>
            </div>

            <%= if Enum.empty?(@all_roles) do %>
              <div class="text-center py-8 text-base-content/70">
                <p>{gettext("No roles available. Create roles first in Role Management.")}</p>
              </div>
            <% end %>

            <div class="modal-action">
              <button type="submit" class="btn btn-primary">
                {gettext("Update Roles")}
              </button>
              <button type="button" phx-click="hide_role_management" class="btn btn-outline">
                {gettext("Cancel")}
              </button>
            </div>
          </form>
        </div>
      </div>
    <% end %>

    <%!-- Users Table --%>
    <div class="bg-base-100">
      <.table_default
        id="users-table"
        variant="zebra"
        class="w-full table-fixed md:table-auto"
        toggleable={true}
        show_toggle={false}
        view_mode={@view_mode}
        view_event="set_view_mode"
        items={@users}
        card_title={fn user -> user.email end}
        card_fields={
          fn user ->
            build_card_fields(
              user,
              @selected_columns,
              @phoenix_kit_current_user,
              @date_time_settings,
              @geolocation_tracking_enabled
            )
          end
        }
      >
        <:toolbar_title>
          <%!-- View + filter controls live on the left (matches the media browser).
               The slot is a plain block, so wrap them in a flex row for gap. --%>
          <div class="flex flex-wrap items-center gap-2">
            <%!-- Display (grid/list) — media-style dropdown --%>
            <% display_options = [
              {"card", gettext("Grid"), "hero-squares-2x2"},
              {"table", gettext("List"), "hero-bars-3-bottom-left"}
            ] %>
            <div class="dropdown">
              <div tabindex="0" role="button" class="btn btn-sm gap-2">
                <.icon
                  name={
                    if @view_mode == "table",
                      do: "hero-bars-3-bottom-left",
                      else: "hero-squares-2x2"
                  }
                  class="w-4 h-4"
                />
                <span class="hidden sm:inline">
                  {if @view_mode == "table", do: gettext("List"), else: gettext("Grid")}
                </span>
                <.icon
                  name="hero-chevron-down"
                  class="hidden sm:inline-block w-3.5 h-3.5 opacity-60"
                />
              </div>
              <ul
                tabindex="0"
                onclick="document.activeElement.blur()"
                class="dropdown-content menu bg-base-100 rounded-box z-20 w-44 p-2 shadow border border-base-200"
              >
                <%= for {val, label, icon} <- display_options do %>
                  <li>
                    <button
                      phx-click="set_view_mode"
                      phx-value-mode={val}
                      class={@view_mode == val && "active"}
                    >
                      <.icon name={icon} class="w-4 h-4" /> {label}
                    </button>
                  </li>
                <% end %>
              </ul>
            </div>
            <%!-- Role Filter (media-style dropdown) --%>
            <% role_options = [
              {"all", gettext("All Users")},
              {"Owner", gettext("Owners Only")},
              {"Admin", gettext("Admins Only")},
              {"User", gettext("Regular Users")}
            ] %>
            <% role_label =
              role_options
              |> Enum.find(fn {v, _} -> v == @filter_role end)
              |> then(&(&1 && elem(&1, 1))) %>
            <div class="dropdown">
              <div
                tabindex="0"
                role="button"
                class={["btn btn-sm gap-2", @filter_role != "all" && "btn-active"]}
              >
                <.icon name="hero-user-group" class="w-4 h-4" />
                <%!-- Show the active role on mobile too (only "All Users" collapses
                     to the bare icon) so the current filter is always legible. --%>
                <span class={["sm:inline", @filter_role == "all" && "hidden"]}>
                  {role_label || gettext("Role")}
                </span>
                <.icon
                  name="hero-chevron-down"
                  class="hidden sm:inline-block w-3.5 h-3.5 opacity-60"
                />
              </div>
              <ul
                tabindex="0"
                onclick="document.activeElement.blur()"
                class="dropdown-content menu bg-base-100 rounded-box z-20 w-44 p-2 shadow border border-base-200"
              >
                <%= for {val, label} <- role_options do %>
                  <li>
                    <button
                      phx-click="filter_by_role"
                      phx-value-role={val}
                      class={@filter_role == val && "active"}
                    >
                      {label}
                    </button>
                  </li>
                <% end %>
              </ul>
            </div>
            <%!-- Account Type Filter (media-style dropdown) --%>
            <%= if @org_accounts_enabled do %>
              <% account_type_options = [
                {"all", gettext("All Types")},
                {"person", gettext("Personal")},
                {"organization", gettext("Organization")}
              ] %>
              <% account_type_label =
                account_type_options
                |> Enum.find(fn {v, _} -> v == @filter_account_type end)
                |> then(&(&1 && elem(&1, 1))) %>
              <div class="dropdown">
                <div
                  tabindex="0"
                  role="button"
                  class={["btn btn-sm gap-2", @filter_account_type != "all" && "btn-active"]}
                >
                  <.icon name="hero-identification" class="w-4 h-4" />
                  <span class={["sm:inline", @filter_account_type == "all" && "hidden"]}>
                    {account_type_label || gettext("Account type")}
                  </span>
                  <.icon
                    name="hero-chevron-down"
                    class="hidden sm:inline-block w-3.5 h-3.5 opacity-60"
                  />
                </div>
                <ul
                  tabindex="0"
                  onclick="document.activeElement.blur()"
                  class="dropdown-content menu bg-base-100 rounded-box z-20 w-44 p-2 shadow border border-base-200"
                >
                  <%= for {val, label} <- account_type_options do %>
                    <li>
                      <button
                        phx-click="filter_by_account_type"
                        phx-value-account_type={val}
                        class={@filter_account_type == val && "active"}
                      >
                        {label}
                      </button>
                    </li>
                  <% end %>
                </ul>
              </div>
            <% end %>
            <%!-- Status filter. Not decoration: without a visible control, a
                  visitor arriving from a dashboard card's link would see a
                  filtered list with nothing on screen saying why. --%>
            <% status_options = [
              {"all", gettext("Any status")},
              {"active", gettext("Active")},
              {"inactive", gettext("Inactive")}
            ] %>
            <% status_label =
              status_options
              |> Enum.find(fn {v, _} -> v == @filter_status end)
              |> then(&(&1 && elem(&1, 1))) %>
            <div class="dropdown">
              <div
                tabindex="0"
                role="button"
                class={["btn btn-sm gap-2", @filter_status != "all" && "btn-active"]}
              >
                <.icon name="hero-power" class="w-4 h-4" />
                <span class={["sm:inline", @filter_status == "all" && "hidden"]}>
                  {status_label || gettext("Status")}
                </span>
                <.icon
                  name="hero-chevron-down"
                  class="hidden sm:inline-block w-3.5 h-3.5 opacity-60"
                />
              </div>
              <ul
                tabindex="0"
                onclick="document.activeElement.blur()"
                class="dropdown-content menu bg-base-100 rounded-box z-20 w-48 p-2 shadow border border-base-200"
              >
                <%= for {val, label} <- status_options do %>
                  <li>
                    <button
                      phx-click="filter_by_status"
                      phx-value-status={val}
                      class={@filter_status == val && "active"}
                    >
                      {label}
                    </button>
                  </li>
                <% end %>
              </ul>
            </div>
            <%!-- Email filter. Not decoration: without a visible control, a
                  visitor arriving from a dashboard card's link would see a
                  filtered list with nothing on screen saying why. --%>
            <% confirmation_options = [
              {"all", gettext("Any email state")},
              {"confirmed", gettext("Email confirmed")},
              {"pending", gettext("Awaiting confirmation")}
            ] %>
            <% confirmation_label =
              confirmation_options
              |> Enum.find(fn {v, _} -> v == @filter_confirmation end)
              |> then(&(&1 && elem(&1, 1))) %>
            <div class="dropdown">
              <div
                tabindex="0"
                role="button"
                class={["btn btn-sm gap-2", @filter_confirmation != "all" && "btn-active"]}
              >
                <.icon name="hero-envelope" class="w-4 h-4" />
                <span class={["sm:inline", @filter_confirmation == "all" && "hidden"]}>
                  {confirmation_label || gettext("Email")}
                </span>
                <.icon
                  name="hero-chevron-down"
                  class="hidden sm:inline-block w-3.5 h-3.5 opacity-60"
                />
              </div>
              <ul
                tabindex="0"
                onclick="document.activeElement.blur()"
                class="dropdown-content menu bg-base-100 rounded-box z-20 w-52 p-2 shadow border border-base-200"
              >
                <%= for {val, label} <- confirmation_options do %>
                  <li>
                    <button
                      phx-click="filter_by_confirmation"
                      phx-value-confirmation={val}
                      class={@filter_confirmation == val && "active"}
                    >
                      {label}
                    </button>
                  </li>
                <% end %>
              </ul>
            </div>
          </div>
        </:toolbar_title>
        <:sort_bar :if={@show_search or @search_query != ""}>
          <form id="users-search-form" phx-change="search" phx-submit="search">
            <label class="input input-sm flex items-center gap-2 w-full">
              <.icon name="hero-magnifying-glass" class="h-4 w-4 opacity-50 shrink-0" />
              <input
                id="users-search-input"
                type="text"
                name="search"
                value={@search_query}
                placeholder={gettext("Search by email, username, or name...")}
                class="grow min-w-0"
                phx-debounce="300"
                autocomplete="off"
                phx-mounted={Phoenix.LiveView.JS.focus()}
                phx-blur="close_search_if_empty"
              />
              <button
                type="button"
                phx-click="clear_search"
                class="btn btn-ghost btn-xs btn-circle shrink-0"
                title={gettext("Close search")}
              >
                <.icon name="hero-x-mark" class="h-3.5 w-3.5" />
              </button>
            </label>
          </form>
        </:sort_bar>
        <:toolbar_actions>
          <%!-- Search (collapsed magnifier opens the row below) + actions on the
               right, mirroring the media browser. --%>
          <button
            type="button"
            phx-click="toggle_search"
            class={[
              "btn btn-sm btn-circle btn-ghost",
              (@show_search or @search_query != "") && "btn-active"
            ]}
            title={gettext("Search")}
          >
            <.icon name="hero-magnifying-glass" class="h-4 w-4" />
          </button>
          <button
            class="btn btn-sm btn-info"
            phx-click="show_column_modal"
            title="Customize columns"
          >
            <.icon name="hero-view-columns" class="h-4 w-4" />
          </button>
          <.link
            navigate={PhoenixKit.Utils.Routes.path("/admin/users/new")}
            class="btn btn-sm btn-primary"
            title={gettext("Add New User")}
          >
            <PhoenixKitWeb.Components.Core.Icons.icon_user_add class="h-4 w-4" />
            <span class="hidden lg:inline">{gettext("Add New User")}</span>
          </.link>
        </:toolbar_actions>
        <:card_header :let={user}>
          <div class="flex items-center gap-3 min-w-0">
            <.user_avatar user={user} size="sm" />
            <div class="flex flex-col gap-0.5 min-w-0">
              <div class="flex items-center gap-2 min-w-0">
                <span class="font-medium text-sm truncate min-w-0">{user.email}</span>
                <%= if user.uuid == @phoenix_kit_current_user.uuid do %>
                  <span class="badge badge-info badge-xs h-auto shrink-0">{gettext("you")}</span>
                <% end %>
              </div>
              <% full_name = User.full_name(user) %>
              <%= if full_name && full_name != "" do %>
                <span class="text-sm text-base-content/60 truncate">{full_name}</span>
              <% end %>
              <%= if user.username do %>
                <div class="text-xs text-base-content/50 truncate">@{user.username}</div>
              <% end %>
            </div>
          </div>
        </:card_header>
        <.table_default_header>
          <.table_default_row>
            <%= for column_id <- @selected_columns do %>
              <%= if should_render_column?(column_id) do %>
                <%!-- On mobile only the email + actions columns show; the rest
                     collapse (hidden md:table-cell), mirroring the media list. --%>
                <.table_default_header_cell class={mobile_col_class(column_id)}>
                  {render_column_header(column_id)}
                </.table_default_header_cell>
              <% end %>
            <% end %>
          </.table_default_row>
        </.table_default_header>

        <.table_default_body>
          <%= if @users == [] do %>
            <.table_default_row>
              <.table_default_cell colspan={
                Enum.count(@selected_columns, &should_render_column?/1)
              }>
                <% filters_active? =
                  @search_query != "" or @filter_role != "all" or
                    @filter_account_type != "all" %>
                <.empty_state
                  icon="hero-users"
                  title={gettext("No users found")}
                  description={
                    if filters_active?,
                      do: gettext("Try adjusting your search or filter criteria."),
                      else: gettext("No users are registered yet.")
                  }
                >
                  <button
                    :if={filters_active?}
                    class="btn btn-outline btn-sm"
                    phx-click="clear_filters"
                  >
                    {gettext("Clear Filters")}
                  </button>
                </.empty_state>
              </.table_default_cell>
            </.table_default_row>
          <% else %>
            <%!-- Host cell for the row_link overlay. Not pinned to "email": that
                 column is `required: false`, so an admin who drops it from the
                 column picker would leave every row wearing `cursor-pointer`
                 with nothing to click. Skip "actions" too — that cell is
                 `relative z-10`, which would float the overlay above its own
                 buttons. --%>
            <% link_column =
              Enum.find(@selected_columns, &(should_render_column?(&1) and &1 != "actions")) %>
            <%= for user <- @users do %>
              <.table_default_row class={[
                "relative transform-gpu",
                link_column && "cursor-pointer"
              ]}>
                <%= for column_id <- @selected_columns do %>
                  <%= if should_render_column?(column_id) do %>
                    <.table_default_cell class={[
                      mobile_col_class(column_id),
                      column_id == "actions" && "relative z-10"
                    ]}>
                      <.row_link
                        :if={column_id == link_column}
                        navigate={PhoenixKit.Utils.Routes.path("/admin/users/view/#{user.uuid}")}
                        label={user.email}
                      />
                      <%= cond do %>
                        <% column_id == "email" -> %>
                          <.link
                            navigate={
                              PhoenixKit.Utils.Routes.path("/admin/users/view/#{user.uuid}")
                            }
                            class="flex items-center gap-3 hover:opacity-80 transition-opacity"
                          >
                            <.user_avatar user={user} size="sm" />
                            <div class="flex flex-col gap-1 min-w-0 max-w-[18rem]">
                              <div class="flex items-center gap-2 min-w-0">
                                <span
                                  class="font-medium link link-hover truncate min-w-0"
                                  title={user.email}
                                >
                                  {user.email}
                                </span>
                                <%= if user.uuid == @phoenix_kit_current_user.uuid do %>
                                  <span class="badge badge-info badge-xs h-auto shrink-0">
                                    {gettext("you")}
                                  </span>
                                <% end %>
                                <%= if @org_accounts_enabled && user.account_type == "organization" do %>
                                  <span class="badge badge-primary badge-xs h-auto shrink-0">
                                    {gettext("Org")}
                                  </span>
                                <% end %>
                              </div>
                              <%= if user.username do %>
                                <div
                                  class="text-sm text-base-content/60 truncate"
                                  title={"@#{user.username}"}
                                >
                                  @{user.username}
                                </div>
                              <% end %>
                              <%!-- Name shown inline on mobile only, since the
                                   full_name column is hidden there. --%>
                              <% mobile_full_name = User.full_name(user) %>
                              <%= if mobile_full_name && mobile_full_name != "" do %>
                                <div
                                  class="md:hidden text-sm text-base-content/60 truncate"
                                  title={mobile_full_name}
                                >
                                  {mobile_full_name}
                                </div>
                              <% end %>
                            </div>
                          </.link>
                        <% column_id == "full_name" -> %>
                          <div class="flex flex-col gap-1 min-w-0 max-w-[18rem]">
                            <%= if user.first_name || user.last_name || user.organization_name do %>
                              <% full_name = PhoenixKit.Users.Auth.User.full_name(user) %>
                              <div class="truncate" title={full_name}>{full_name}</div>
                            <% else %>
                              <div class="text-base-content/50">-</div>
                            <% end %>
                            <%= if @org_accounts_enabled && user.organization do %>
                              <div
                                class="text-xs text-base-content/60 truncate"
                                title={user.organization.organization_name}
                              >
                                {user.organization.organization_name}
                              </div>
                            <% end %>
                          </div>
                        <% column_id == "role" -> %>
                          <% user_roles = get_user_roles(user) %>
                          <div class="flex flex-wrap gap-1">
                            <%= for role_name <- user_roles do %>
                              <.role_badge
                                role={%{name: role_name, is_system_role: false}}
                                size={:sm}
                              />
                            <% end %>
                            <%= if Enum.empty?(user_roles) do %>
                              <span class="badge badge-ghost badge-sm h-auto">
                                {gettext("No roles")}
                              </span>
                            <% end %>
                          </div>
                        <% column_id == "status" -> %>
                          <div class="flex flex-col gap-1">
                            <%= if user.confirmed_at do %>
                              <span class="badge badge-success badge-sm h-auto">
                                {gettext("Email Confirmed")}
                              </span>
                            <% else %>
                              <span class="badge badge-warning badge-sm h-auto">
                                {gettext("Email Pending")}
                              </span>
                            <% end %>
                            <%= if user.is_active do %>
                              <span class="badge badge-outline badge-xs h-auto">
                                {gettext("Active")}
                              </span>
                            <% else %>
                              <span class="badge badge-error badge-xs h-auto">
                                {gettext("Inactive")}
                              </span>
                            <% end %>
                          </div>
                        <% column_id == "registered" -> %>
                          <div class="text-sm">
                            {UtilsDate.format_date_with_user_timezone_cached(
                              user.inserted_at,
                              @phoenix_kit_current_user,
                              @date_time_settings
                            )}
                          </div>
                        <% column_id == "location" -> %>
                          <div class="text-sm">
                            <div class="flex flex-col gap-1">
                              <%= cond do %>
                                <% !@geolocation_tracking_enabled -> %>
                                  <div
                                    class="tooltip tooltip-right"
                                    data-tip={
                                      gettext(
                                        "Location tracking is off, so no user shows a location. Turn it on in Settings > Users."
                                      )
                                    }
                                  >
                                    <%!-- relative z-10: this link sits inside a row that
                                         carries the row_link overlay, and without it the
                                         overlay swallows the click and opens the user. --%>
                                    <.link
                                      navigate={
                                        PhoenixKit.Utils.Routes.path("/admin/settings/users")
                                      }
                                      class="relative z-10 inline-flex items-center gap-1 text-xs text-base-content/50 hover:text-primary hover:underline"
                                    >
                                      <.icon name="hero-information-circle" class="w-3.5 h-3.5" />
                                      {gettext("Tracking disabled")}
                                    </.link>
                                  </div>
                                <% user.registration_country || user.registration_region || user.registration_city || user.registration_ip -> %>
                                  <%= if user.registration_country do %>
                                    <div class="flex items-center gap-1">
                                      <span class="badge badge-outline badge-xs h-auto">
                                        {user.registration_country}
                                      </span>
                                    </div>
                                  <% end %>
                                  <%= if user.registration_region || user.registration_city do %>
                                    <div class="text-xs text-base-content/70">
                                      <%= if user.registration_region && user.registration_city do %>
                                        {user.registration_region}, {user.registration_city}
                                      <% else %>
                                        {user.registration_region || user.registration_city}
                                      <% end %>
                                    </div>
                                  <% end %>
                                  <%= if user.registration_ip do %>
                                    <div class="text-xs text-base-content/50 font-mono">
                                      {user.registration_ip}
                                    </div>
                                  <% end %>
                                <% true -> %>
                                  <div class="text-xs text-base-content/50">
                                    {gettext("No data")}
                                  </div>
                              <% end %>
                            </div>
                          </div>
                        <% column_id == "last_confirmed" -> %>
                          <div class="text-sm">
                            <%= if user.confirmed_at do %>
                              {UtilsDate.format_date_with_user_timezone_cached(
                                user.confirmed_at,
                                @phoenix_kit_current_user,
                                @date_time_settings
                              )}
                            <% else %>
                              {gettext("Never")}
                            <% end %>
                          </div>
                        <% column_id == "actions" -> %>
                          <.table_row_menu
                            id={"user-menu-#{user.uuid}"}
                            label={gettext("User actions")}
                          >
                            <%!-- View --%>
                            <.table_row_menu_link
                              navigate={
                                PhoenixKit.Utils.Routes.path("/admin/users/view/#{user.uuid}")
                              }
                              icon="hero-eye"
                              label={gettext("View")}
                            />
                            <%!-- Edit --%>
                            <.table_row_menu_link
                              navigate={
                                PhoenixKit.Utils.Routes.path(
                                  "/admin/users/edit/#{user.uuid}?return_to=list"
                                )
                              }
                              icon="hero-pencil"
                              label={gettext("Edit")}
                              variant="secondary"
                            />
                            <%!-- Sign in as this user. Shown only where it would
                                 be allowed; the POST re-decides server-side. --%>
                            <.table_row_menu_link
                              :if={user.uuid in @impersonable_uuids}
                              href={
                                PhoenixKit.Utils.Routes.path(
                                  "/users/session/impersonate/#{user.uuid}"
                                )
                              }
                              method="post"
                              data-confirm={gettext("Sign in as %{email}?", email: user.email)}
                              icon="hero-arrow-right-end-on-rectangle"
                              label={gettext("Sign in as user")}
                              variant="warning"
                            />
                            <%!-- Role Management --%>
                            <%= if user.uuid != @phoenix_kit_current_user.uuid do %>
                              <.table_row_menu_button
                                phx-click="show_role_management"
                                phx-value-user_uuid={user.uuid}
                                icon="hero-user-group"
                                label={gettext("Roles")}
                                variant="primary"
                              />
                            <% end %>
                            <%!-- Current user: Settings --%>
                            <%= if user.uuid == @phoenix_kit_current_user.uuid do %>
                              <.table_row_menu_link
                                navigate={PhoenixKit.Utils.Routes.user_settings_path()}
                                icon="hero-cog-6-tooth"
                                label={gettext("Settings")}
                                variant="info"
                              />
                            <% end %>
                            <%!-- Non-owner actions: confirmation, status. Permanent
                                  deletion is deliberately NOT offered from this list —
                                  it's a one-way, all-the-related-data action that
                                  belongs on the user's own page, where the admin sees
                                  exactly what it will touch before confirming
                                  (PhoenixKitWeb.Live.Users.UserDetails). --%>
                            <%= if user.uuid != @phoenix_kit_current_user.uuid && get_primary_role_name_unsafe(user) != "Owner" do %>
                              <.table_row_menu_divider />
                              <%!-- Confirmation answers to the same rank rule as
                                    status: `toggle_user_confirmation/2` refuses
                                    an out-of-rank target, and unconfirming locks
                                    the account out of every gate that honours
                                    `require_email_confirmation`. --%>
                              <%= if Auth.can_manage_user_status?(user, @actor_with_roles) do %>
                                <.table_row_menu_button
                                  phx-click="request_confirmation_toggle"
                                  phx-value-user_uuid={user.uuid}
                                  phx-value-is_confirmed={to_string(!!user.confirmed_at)}
                                  icon={
                                    if user.confirmed_at,
                                      do: "hero-envelope-open",
                                      else: "hero-envelope"
                                  }
                                  label={
                                    if user.confirmed_at,
                                      do: gettext("Unconfirm Email"),
                                      else: gettext("Confirm Email")
                                  }
                                  variant={if user.confirmed_at, do: "success", else: "warning"}
                                />
                                <.table_row_menu_button
                                  phx-click="request_status_toggle"
                                  phx-value-user_uuid={user.uuid}
                                  phx-value-is_active={to_string(user.is_active)}
                                  icon={
                                    if user.is_active,
                                      do: "hero-check-circle",
                                      else: "hero-x-circle"
                                  }
                                  label={
                                    if user.is_active,
                                      do: gettext("Deactivate"),
                                      else: gettext("Activate")
                                  }
                                  variant={if user.is_active, do: "success", else: "warning"}
                                />
                              <% end %>
                            <% end %>
                          </.table_row_menu>
                        <% true -> %>
                          <%!-- Handle custom fields and other columns --%>
                          {render_column_cell(
                            user,
                            column_id,
                            @phoenix_kit_current_user,
                            @date_time_settings
                          )}
                      <% end %>
                    </.table_default_cell>
                  <% end %>
                <% end %>
              </.table_default_row>
            <% end %>
          <% end %>
        </.table_default_body>
        <:card_actions :let={user}>
          <.table_row_menu id={"user-card-menu-#{user.uuid}"} label={gettext("User actions")}>
            <.table_row_menu_link
              navigate={PhoenixKit.Utils.Routes.path("/admin/users/view/#{user.uuid}")}
              icon="hero-eye"
              label={gettext("View")}
            />
            <.table_row_menu_link
              navigate={
                PhoenixKit.Utils.Routes.path("/admin/users/edit/#{user.uuid}?return_to=list")
              }
              icon="hero-pencil"
              label={gettext("Edit")}
              variant="secondary"
            />
            <.table_row_menu_link
              :if={user.uuid in @impersonable_uuids}
              href={PhoenixKit.Utils.Routes.path("/users/session/impersonate/#{user.uuid}")}
              method="post"
              data-confirm={gettext("Sign in as %{email}?", email: user.email)}
              icon="hero-arrow-right-end-on-rectangle"
              label={gettext("Sign in as user")}
              variant="warning"
            />
            <%= if user.uuid != @phoenix_kit_current_user.uuid do %>
              <.table_row_menu_button
                phx-click="show_role_management"
                phx-value-user_uuid={user.uuid}
                icon="hero-user-group"
                label={gettext("Roles")}
                variant="primary"
              />
            <% end %>
            <%= if user.uuid == @phoenix_kit_current_user.uuid do %>
              <.table_row_menu_link
                navigate={PhoenixKit.Utils.Routes.user_settings_path()}
                icon="hero-cog-6-tooth"
                label={gettext("Settings")}
                variant="info"
              />
            <% end %>
            <%= if user.uuid != @phoenix_kit_current_user.uuid && get_primary_role_name_unsafe(user) != "Owner" do %>
              <.table_row_menu_divider />
              <%!-- Same rank rule the context enforces, so the menu cannot offer
                    an action the request would refuse — for the confirmation
                    switch as much as for the status one. --%>
              <%= if Auth.can_manage_user_status?(user, @actor_with_roles) do %>
                <.table_row_menu_button
                  phx-click="request_confirmation_toggle"
                  phx-value-user_uuid={user.uuid}
                  phx-value-is_confirmed={to_string(!!user.confirmed_at)}
                  icon={if user.confirmed_at, do: "hero-envelope-open", else: "hero-envelope"}
                  label={
                    if user.confirmed_at,
                      do: gettext("Unconfirm Email"),
                      else: gettext("Confirm Email")
                  }
                  variant={if user.confirmed_at, do: "success", else: "warning"}
                />
                <.table_row_menu_button
                  phx-click="request_status_toggle"
                  phx-value-user_uuid={user.uuid}
                  phx-value-is_active={to_string(user.is_active)}
                  icon={if user.is_active, do: "hero-check-circle", else: "hero-x-circle"}
                  label={if user.is_active, do: gettext("Deactivate"), else: gettext("Activate")}
                  variant={if user.is_active, do: "success", else: "warning"}
                />
              <% end %>
            <% end %>
          </.table_row_menu>
        </:card_actions>
      </.table_default>

      <%!-- Pagination --%>
      <div
        :if={@users != []}
        class="flex flex-col sm:flex-row justify-center items-center gap-4 p-4 border-t border-base-300"
      >
        <.pagination_controls
          page={@page}
          total_pages={@total_pages}
          build_url={&url_state_path(assigns, page: &1)}
        />
        <.page_size_selector
          id="users-per-page"
          value={@per_page}
          auto_fit
          auto={@auto_fit}
          table_id="users-table"
        />
      </div>
    </div>

    <%!-- Enhanced Stats --%>
    <div class="mt-8">
      <h3 class="text-lg font-semibold text-base-content mb-6">{gettext("User Statistics")}</h3>

      <%!-- Main Role Stats --%>
      <div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
        <PhoenixKitWeb.Components.Core.StatCard.stat_card
          compact={true}
          rounded="xl"
          value={@total_users}
          title={gettext("Total Users")}
          subtitle={gettext("All registered accounts")}
        >
          <:icon>
            <.icon name="hero-users" class="w-5 h-5" />
          </:icon>
        </PhoenixKitWeb.Components.Core.StatCard.stat_card>

        <PhoenixKitWeb.Components.Core.StatCard.stat_card
          compact={true}
          rounded="xl"
          value={@total_owners}
          title={gettext("System Owners")}
          subtitle={gettext("Complete system authority")}
        >
          <:icon>
            <.icon name="hero-shield-check" class="w-5 h-5" />
          </:icon>
        </PhoenixKitWeb.Components.Core.StatCard.stat_card>

        <PhoenixKitWeb.Components.Core.StatCard.stat_card
          compact={true}
          rounded="xl"
          value={@total_admins}
          title={gettext("Administrators")}
          subtitle={gettext("Management privileges")}
        >
          <:icon>
            <.icon name="hero-star" class="w-5 h-5" />
          </:icon>
        </PhoenixKitWeb.Components.Core.StatCard.stat_card>

        <PhoenixKitWeb.Components.Core.StatCard.stat_card
          compact={true}
          rounded="xl"
          value={@total_regular_users}
          title={gettext("Regular Users")}
          subtitle={gettext("Standard access level")}
        >
          <:icon>
            <.icon name="hero-user" class="w-5 h-5" />
          </:icon>
        </PhoenixKitWeb.Components.Core.StatCard.stat_card>
      </div>

      <%!-- Secondary Stats --%>
      <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
        <%!-- Active Users --%>
        <PhoenixKitWeb.Components.Core.StatCard.stat_card
          compact={true}
          rounded="2xl"
          value={@active_users}
          title={gettext("Active Users")}
          subtitle={gettext("Currently online")}
        >
          <:icon>
            <.icon name="hero-check-circle-solid" class="w-5 h-5" />
          </:icon>
        </PhoenixKitWeb.Components.Core.StatCard.stat_card>

        <%!-- Inactive Users --%>
        <PhoenixKitWeb.Components.Core.StatCard.stat_card
          compact={true}
          rounded="2xl"
          value={@inactive_users}
          title={gettext("Inactive Users")}
          subtitle={gettext("Disabled accounts")}
        >
          <:icon>
            <.icon name="hero-x-circle-solid" class="w-5 h-5" />
          </:icon>
        </PhoenixKitWeb.Components.Core.StatCard.stat_card>

        <%!-- Confirmed Users --%>
        <PhoenixKitWeb.Components.Core.StatCard.stat_card
          compact={true}
          rounded="2xl"
          value={@confirmed_users}
          title={gettext("Email Confirmed")}
          subtitle={gettext("Verified emails")}
        >
          <:icon>
            <.icon name="hero-envelope" class="w-5 h-5" />
          </:icon>
        </PhoenixKitWeb.Components.Core.StatCard.stat_card>

        <%!-- Pending Users --%>
        <PhoenixKitWeb.Components.Core.StatCard.stat_card
          compact={true}
          rounded="2xl"
          value={@pending_users}
          title={gettext("Pending Email")}
          subtitle={gettext("Awaiting confirmation")}
        >
          <:icon>
            <.icon name="hero-information-circle-solid" class="w-5 h-5" />
          </:icon>
        </PhoenixKitWeb.Components.Core.StatCard.stat_card>
      </div>
    </div>

    <%!-- Column Selection Modal --%>
    <%= if @show_column_modal do %>
      <div class="modal modal-open" id="column-modal">
        <div class="modal-box max-w-5xl w-11/12 max-h-[90vh] flex flex-col">
          <h3 class="font-bold text-xl mb-4 shrink-0">{gettext("Customize Table Columns")}</h3>
          <p class="text-base-content/70 mb-6 shrink-0">
            {gettext(
              "Drag to reorder selected columns above, or add new columns from the options below. Actions column is always included."
            )}
          </p>

          <form
            phx-submit="update_table_columns"
            id="column-form"
            class="flex flex-col flex-1 min-h-0"
          >
            <%!-- Hidden input to store the column order for form submission --%>
            <input
              type="hidden"
              name="column_order"
              id="column-order-input"
              value={Enum.join(@temp_selected_columns || @selected_columns, ",")}
            />

            <div class="flex flex-col lg:flex-row gap-6 mb-6 flex-1 min-h-0 overflow-y-auto lg:overflow-visible">
              <%!-- Selected Columns Section (Left/Top) --%>
              <div class="flex-1">
                <div class="flex items-center justify-between mb-3">
                  <h4 class="text-sm font-semibold text-base-content/80 uppercase tracking-wide">
                    {gettext("Selected Columns")}
                  </h4>
                  <span class="text-xs text-base-content/60">
                    {gettext("Drag to reorder")}
                  </span>
                </div>
                <.draggable_list
                  id="selected-columns-container"
                  items={
                    Enum.reject(@temp_selected_columns || @selected_columns, &(&1 == "actions"))
                  }
                  item_id={fn col_id -> col_id end}
                  on_reorder="reorder_selected_columns"
                  layout={:list}
                  gap="space-y-2"
                  class="min-h-[120px] lg:min-h-[200px] lg:max-h-[400px] lg:overflow-y-auto border-2 border-dashed border-base-300 rounded-lg p-3 bg-base-50"
                  item_class="flex items-center p-3 rounded-lg bg-primary/10 border border-primary/30 hover:bg-primary/20 transition-colors"
                >
                  <:item :let={column_id}>
                    <% column_meta = PhoenixKit.Users.TableColumns.get_column_metadata(column_id) %>
                    <div class="drag-handle text-primary/60 mr-3">
                      <.icon name="hero-bars-3" class="h-5 w-5" />
                    </div>
                    <div class="flex flex-col flex-1">
                      <span class="fieldset-legend font-medium text-base-content">
                        {(column_meta && column_meta.label) || column_id}
                      </span>
                    </div>
                    <button
                      type="button"
                      class="btn btn-ghost btn-xs btn-circle text-error/60 hover:text-error hover:bg-error/10"
                      phx-click="remove_column"
                      phx-value-column_id={column_id}
                      title={gettext("Remove column")}
                    >
                      <.icon name="hero-x-mark" class="h-4 w-4" />
                    </button>
                  </:item>
                </.draggable_list>

                <%!-- Empty state for selected columns --%>
                <%= if length(Enum.reject(@temp_selected_columns || @selected_columns, &(&1 == "actions"))) == 0 do %>
                  <div class="text-center py-12 text-base-content/40 border-2 border-dashed border-base-300 rounded-lg mt-2">
                    <.icon name="hero-clipboard-document-list" class="h-12 w-12 mx-auto mb-3" />
                    <p class="text-sm">{gettext("No columns selected yet")}</p>
                    <p class="text-xs mt-1">{gettext("Add columns from the options below")}</p>
                  </div>
                <% end %>
              </div>

              <%!-- Available Fields Section (Right/Bottom) --%>
              <div class="flex-1">
                <div class="flex items-center justify-between mb-3">
                  <h4 class="text-sm font-semibold text-base-content/80 uppercase tracking-wide">
                    {gettext("Available Fields")}
                  </h4>
                  <span class="text-xs text-base-content/60">
                    {gettext("Click to add")}
                  </span>
                </div>
                <div class="lg:max-h-[400px] lg:overflow-y-auto border border-base-200 rounded-lg bg-base-100">
                  <%!-- Standard Fields Section --%>
                  <%= if map_size(@available_columns.standard) > 0 do %>
                    <div class="p-3">
                      <h5 class="text-xs font-semibold text-base-content/60 mb-2 uppercase tracking-wide">
                        {gettext("Standard Fields")}
                      </h5>
                      <div class="space-y-1">
                        <%= for {column_id, column_meta} <- @available_columns.standard do %>
                          <%= unless column_id in (@temp_selected_columns || @selected_columns) or column_id == "actions" do %>
                            <button
                              type="button"
                              class="available-field w-full flex items-center p-2 rounded-lg hover:bg-base-200 transition-colors text-left border border-transparent hover:border-base-300"
                              phx-click="add_column"
                              phx-value-column_id={column_id}
                              data-column-id={column_id}
                              data-section="available-standard"
                            >
                              <div class="flex flex-col flex-1">
                                <span class="fieldset-legend font-medium text-sm text-base-content">
                                  {column_meta.label}
                                </span>
                              </div>
                              <div class="text-success/60">
                                <svg
                                  xmlns="http://www.w3.org/2000/svg"
                                  class="h-4 w-4"
                                  fill="none"
                                  viewBox="0 0 24 24"
                                  stroke="currentColor"
                                >
                                  <path
                                    stroke-linecap="round"
                                    stroke-linejoin="round"
                                    stroke-width="2"
                                    d="M12 4v16m8-8H4"
                                  />
                                </svg>
                              </div>
                            </button>
                          <% end %>
                        <% end %>
                      </div>
                    </div>
                  <% end %>

                  <%!-- Separator Line --%>
                  <%= if map_size(@available_columns.standard) > 0 and map_size(@available_columns.custom) > 0 do %>
                    <div class="border-t border-base-200"></div>
                  <% end %>

                  <%!-- Custom Fields Section --%>
                  <%= if map_size(@available_columns.custom) > 0 do %>
                    <div class="p-3">
                      <h5 class="text-xs font-semibold text-base-content/60 mb-2 uppercase tracking-wide">
                        {gettext("Custom Fields")}
                      </h5>
                      <div class="space-y-1">
                        <%= for {column_id, column_meta} <- @available_columns.custom do %>
                          <%= unless column_id in (@temp_selected_columns || @selected_columns) do %>
                            <button
                              type="button"
                              class="available-field w-full flex items-center p-2 rounded-lg hover:bg-base-200 transition-colors text-left border border-transparent hover:border-base-300"
                              phx-click="add_column"
                              phx-value-column_id={column_id}
                              data-column-id={column_id}
                              data-section="available-custom"
                            >
                              <div class="flex flex-col flex-1">
                                <span class="fieldset-legend font-medium text-sm text-base-content">
                                  {column_meta.label}
                                </span>
                              </div>
                              <div class="text-success/60">
                                <svg
                                  xmlns="http://www.w3.org/2000/svg"
                                  class="h-4 w-4"
                                  fill="none"
                                  viewBox="0 0 24 24"
                                  stroke="currentColor"
                                >
                                  <path
                                    stroke-linecap="round"
                                    stroke-linejoin="round"
                                    stroke-width="2"
                                    d="M12 4v16m8-8H4"
                                  />
                                </svg>
                              </div>
                            </button>
                          <% end %>
                        <% end %>
                      </div>
                    </div>
                  <% end %>

                  <%!-- No Available Fields Message --%>
                  <%= if get_available_fields_count(@available_columns, @temp_selected_columns || @selected_columns) == 0 do %>
                    <div class="text-center py-8 text-base-content/40">
                      <p class="text-sm">{gettext("All fields are already selected")}</p>
                    </div>
                  <% end %>
                </div>
              </div>
            </div>

            <div class="modal-action shrink-0 flex-wrap pt-4 border-t border-base-200">
              <button type="submit" class="btn btn-primary">
                {gettext("Apply Changes")}
              </button>
              <button type="button" class="btn btn-outline" phx-click="reset_to_defaults">
                {gettext("Default")}
              </button>
              <button type="button" class="btn btn-ghost" phx-click="hide_column_modal">
                {gettext("Cancel")}
              </button>
            </div>
          </form>
        </div>
        <div class="modal-backdrop" phx-click="hide_column_modal"></div>
      </div>
    <% end %>

    <%!-- Confirmation Modal --%>
    <%= if assigns[:confirmation_modal] && @confirmation_modal.show do %>
      <div class="modal modal-open">
        <div class="modal-box">
          <h3 class="font-bold text-lg">{@confirmation_modal.title}</h3>
          <p class="py-4">{@confirmation_modal.message}</p>
          <div class="modal-action">
            <button class="btn btn-ghost" phx-click="cancel_confirmation">
              {gettext("Cancel")}
            </button>
            <button
              class="btn btn-primary"
              phx-click="confirm_action"
              phx-value-action={@confirmation_modal.action}
              phx-value-user_uuid={@confirmation_modal.user_uuid}
            >
              {@confirmation_modal.button_text}
            </button>
          </div>
        </div>
      </div>
    <% end %>
  </div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>