Packages

phoenix_kit

1.7.82
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 user_details.html.heex
Raw

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

<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title={@page_title}
current_path={@url_path}
project_title={@project_title}
current_locale={@current_locale}
>
<div class="container flex-col mx-auto px-4 py-6">
<.admin_page_header back={PhoenixKit.Utils.Routes.path("/admin/users")}>
<h1 class="text-xl sm:text-2xl lg:text-3xl font-bold text-base-content">{@page_title}</h1>
<p class="text-sm text-base-content/60 mt-0.5">{@user.email}</p>
<:actions>
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/users/edit/#{@user.uuid}")}
class="btn btn-primary btn-sm"
>
<PhoenixKitWeb.Components.Core.Icons.icon_edit /> {gettext("Edit")}
</.link>
<button
type="button"
class="btn btn-error btn-sm btn-outline"
phx-click="show_delete_modal"
>
<PhoenixKitWeb.Components.Core.Icons.icon_trash /> {gettext("Delete")}
</button>
</:actions>
</.admin_page_header>
<%!-- Tabs Navigation --%>
<div class="flex justify-center mb-6">
<div role="tablist" class="tabs tabs-boxed bg-base-200 p-1">
<button
type="button"
role="tab"
class={"tab gap-2 #{if @active_tab == "profile", do: "tab-active"}"}
phx-click="change_tab"
phx-value-tab="profile"
>
<.icon name="hero-user" class="w-4 h-4" />
{gettext("Profile")}
</button>
<%= if @connections_enabled do %>
<button
type="button"
role="tab"
class={"tab gap-2 #{if @active_tab == "connections", do: "tab-active"}"}
phx-click="change_tab"
phx-value-tab="connections"
>
<.icon name="hero-user-group" class="w-4 h-4" />
{gettext("Connections")}
</button>
<% end %>
<button
type="button"
role="tab"
class={"tab gap-2 #{if @active_tab == "notes", do: "tab-active"}"}
phx-click="change_tab"
phx-value-tab="notes"
>
<.icon name="hero-document-text" class="w-4 h-4" />
{gettext("Notes")}
<%= if length(@admin_notes) > 0 do %>
<span class="badge badge-sm h-auto">{length(@admin_notes)}</span>
<% end %>
</button>
</div>
</div>
<%!-- Tab Content --%>
<div class="max-w-4xl mx-auto">
<%= if @active_tab == "profile" do %>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<%!-- Left Column: Avatar and Quick Stats --%>
<div class="lg:col-span-1">
<div class="card bg-base-200 shadow-sm">
<div class="card-body items-center text-center">
<%!-- Avatar --%>
<div class="mb-4">
<.user_avatar user={@user} size="lg" class="!w-24 !h-24 !text-3xl" />
</div>
<%!-- Name --%>
<h2 class="card-title">{user_display_name(@user)}</h2>
<%!-- Username --%>
<%= if @user.username do %>
<p class="text-base-content/70">@{@user.username}</p>
<% end %>
<%!-- Status Badges --%>
<div class="flex flex-wrap gap-2 mt-4 justify-center">
<%= if @user.is_active do %>
<span class="badge badge-success">{gettext("Active")}</span>
<% else %>
<span class="badge badge-error">{gettext("Inactive")}</span>
<% end %>
<%= if @user.confirmed_at do %>
<span class="badge badge-info">{gettext("Confirmed")}</span>
<% else %>
<span class="badge badge-warning">{gettext("Unconfirmed")}</span>
<% end %>
</div>
<%!-- Roles --%>
<%= if @user.roles && length(@user.roles) > 0 do %>
<div class="mt-4">
<h3 class="text-sm font-semibold text-base-content/70 mb-2">
{gettext("Roles")}
</h3>
<div class="flex flex-wrap gap-1 justify-center">
<%= for role <- @user.roles do %>
<.role_badge role={role} size={:sm} />
<% end %>
</div>
</div>
<% end %>
</div>
</div>
</div>
<%!-- Right Column: Details --%>
<div class="lg:col-span-2 space-y-6">
<%!-- Basic Information --%>
<div class="card bg-base-200 shadow-sm">
<div class="card-body">
<h3 class="card-title text-lg">{gettext("Basic Information")}</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
<div>
<label class="text-sm font-medium text-base-content/70">
{gettext("Email")}
</label>
<p class="text-base-content">{@user.email}</p>
</div>
<div>
<label class="text-sm font-medium text-base-content/70">
{gettext("Username")}
</label>
<p class="text-base-content">{@user.username || "-"}</p>
</div>
<div>
<label class="text-sm font-medium text-base-content/70">
{gettext("First Name")}
</label>
<p class="text-base-content">{@user.first_name || "-"}</p>
</div>
<div>
<label class="text-sm font-medium text-base-content/70">
{gettext("Last Name")}
</label>
<p class="text-base-content">{@user.last_name || "-"}</p>
</div>
<div>
<label class="text-sm font-medium text-base-content/70">
{gettext("Timezone")}
</label>
<p class="text-base-content">{format_timezone(@user.user_timezone)}</p>
</div>
</div>
</div>
</div>
<%!-- Account Information --%>
<div class="card bg-base-200 shadow-sm">
<div class="card-body">
<h3 class="card-title text-lg">{gettext("Account Information")}</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
<div>
<label class="text-sm font-medium text-base-content/70">
{gettext("User ID")}
</label>
<p class="text-base-content font-mono text-sm">{@user.uuid}</p>
</div>
<div>
<label class="text-sm font-medium text-base-content/70">
{gettext("Status")}
</label>
<p class="text-base-content">
{if @user.is_active, do: gettext("Active"), else: gettext("Inactive")}
</p>
</div>
<div>
<label class="text-sm font-medium text-base-content/70">
{gettext("Email Confirmed")}
</label>
<p class="text-base-content">
<%= if @user.confirmed_at do %>
{UtilsDate.format_datetime_with_user_format(@user.confirmed_at)}
<% else %>
{gettext("Not confirmed")}
<% end %>
</p>
</div>
<div>
<label class="text-sm font-medium text-base-content/70">
{gettext("Registered")}
</label>
<p class="text-base-content">
{UtilsDate.format_datetime_with_user_format(@user.inserted_at)}
</p>
</div>
<div>
<label class="text-sm font-medium text-base-content/70">
{gettext("Last Updated")}
</label>
<p class="text-base-content">
{UtilsDate.format_datetime_with_user_format(@user.updated_at)}
</p>
</div>
</div>
</div>
</div>
<%!-- Registration Details --%>
<%= if @user.registration_ip || format_location(@user) do %>
<div class="card bg-base-200 shadow-sm">
<div class="card-body">
<h3 class="card-title text-lg">{gettext("Registration Details")}</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
<%= if @user.registration_ip do %>
<div>
<label class="text-sm font-medium text-base-content/70">
{gettext("Registration IP")}
</label>
<p class="text-base-content font-mono text-sm">{@user.registration_ip}</p>
</div>
<% end %>
<%= if format_location(@user) do %>
<div>
<label class="text-sm font-medium text-base-content/70">
{gettext("Registration Location")}
</label>
<p class="text-base-content">{format_location(@user)}</p>
</div>
<% end %>
</div>
</div>
</div>
<% end %>
<%!-- Custom Fields --%>
<%= if @custom_field_definitions && length(@custom_field_definitions) > 0 do %>
<div class="card bg-base-200 shadow-sm">
<div class="card-body">
<h3 class="card-title text-lg">{gettext("Custom Fields")}</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
<%= for field <- @custom_field_definitions do %>
<div>
<label class="text-sm font-medium text-base-content/70">
{field["label"]}
</label>
<p class="text-base-content">
{format_custom_field_value(
get_custom_field_value(@user, field["key"]),
field["type"],
field["key"]
)}
</p>
</div>
<% end %>
</div>
</div>
</div>
<% else %>
<%= if @user.custom_fields && map_size(@user.custom_fields) > 0 do %>
<div class="card bg-base-200 shadow-sm">
<div class="card-body">
<h3 class="card-title text-lg">{gettext("Custom Fields")}</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
<%= for {key, value} <- Enum.sort(@user.custom_fields) do %>
<div>
<label class="text-sm font-medium text-base-content/70">
{Phoenix.Naming.humanize(key)}
</label>
<p class="text-base-content">
{format_custom_field_value(value, nil, key)}
</p>
</div>
<% end %>
</div>
</div>
</div>
<% end %>
<% end %>
</div>
</div>
<% end %>
<%!-- Connections Tab Content --%>
<%= if @active_tab == "connections" and @connections_enabled do %>
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
<%!-- Followers --%>
<div class="card bg-base-200 shadow-sm">
<div class="card-body items-center text-center p-4">
<.icon name="hero-users" class="w-8 h-8 text-primary" />
<div class="text-2xl font-bold">{@connections_stats.followers}</div>
<div class="text-sm text-base-content/70">{gettext("Followers")}</div>
</div>
</div>
<%!-- Following --%>
<div class="card bg-base-200 shadow-sm">
<div class="card-body items-center text-center p-4">
<.icon name="hero-user-plus" class="w-8 h-8 text-secondary" />
<div class="text-2xl font-bold">{@connections_stats.following}</div>
<div class="text-sm text-base-content/70">{gettext("Following")}</div>
</div>
</div>
<%!-- Connections --%>
<div class="card bg-base-200 shadow-sm">
<div class="card-body items-center text-center p-4">
<.icon name="hero-link" class="w-8 h-8 text-accent" />
<div class="text-2xl font-bold">{@connections_stats.connections}</div>
<div class="text-sm text-base-content/70">{gettext("Connections")}</div>
</div>
</div>
<%!-- Pending --%>
<div class="card bg-base-200 shadow-sm">
<div class="card-body items-center text-center p-4">
<.icon name="hero-clock" class="w-8 h-8 text-warning" />
<div class="text-2xl font-bold">{@connections_stats.pending}</div>
<div class="text-sm text-base-content/70">{gettext("Pending")}</div>
</div>
</div>
<%!-- Blocked --%>
<div class="card bg-base-200 shadow-sm">
<div class="card-body items-center text-center p-4">
<.icon name="hero-no-symbol" class="w-8 h-8 text-error" />
<div class="text-2xl font-bold">{@connections_stats.blocked}</div>
<div class="text-sm text-base-content/70">{gettext("Blocked")}</div>
</div>
</div>
</div>
<% end %>
<%!-- Notes Tab Content --%>
<%= if @active_tab == "notes" do %>
<div class="space-y-6">
<%!-- Add Note Form --%>
<div class="card bg-base-200 shadow-sm">
<div class="card-body">
<h3 class="card-title text-lg">
<%= if @editing_note_uuid do %>
{gettext("Edit Note")}
<% else %>
{gettext("Add Note")}
<% end %>
</h3>
<.form
for={@note_form}
phx-submit={if @editing_note_uuid, do: "update_note", else: "add_note"}
class="space-y-4"
>
<div class="form-control">
<textarea
name="admin_note[content]"
class="textarea textarea-bordered w-full h-24"
placeholder={gettext("Write a note about this user...")}
required
>{Phoenix.HTML.Form.input_value(@note_form, :content)}</textarea>
</div>
<div class="flex justify-end gap-2">
<%= if @editing_note_uuid do %>
<button type="button" class="btn btn-ghost" phx-click="cancel_edit">
{gettext("Cancel")}
</button>
<button type="submit" class="btn btn-primary">
<.icon name="hero-check" class="w-4 h-4" />
{gettext("Update Note")}
</button>
<% else %>
<button type="submit" class="btn btn-primary">
<.icon name="hero-plus" class="w-4 h-4" />
{gettext("Add Note")}
</button>
<% end %>
</div>
</.form>
</div>
</div>
<%!-- Notes List --%>
<%= if length(@admin_notes) > 0 do %>
<div class="card bg-base-200 shadow-sm">
<div class="card-body">
<h3 class="card-title text-lg">
{gettext("Notes")}
<span class="badge badge-neutral">{length(@admin_notes)}</span>
</h3>
<div class="divide-y divide-base-300">
<%= for note <- @admin_notes do %>
<div class="py-4 first:pt-0 last:pb-0">
<div class="flex items-start justify-between gap-4">
<div class="flex items-start gap-3 flex-1">
<.user_avatar user={note.author} size="sm" class="!w-8 !h-8 !text-sm" />
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<span class="font-medium text-base-content">
<%= if note.author.first_name || note.author.last_name do %>
{note.author.first_name} {note.author.last_name}
<% else %>
{note.author.email}
<% end %>
</span>
<span class="text-sm text-base-content/50">
<.time_ago datetime={note.inserted_at} />
</span>
<%= if note.updated_at != note.inserted_at do %>
<span class="text-xs text-base-content/40">
({gettext("edited")})
</span>
<% end %>
</div>
<p class="mt-1 text-base-content whitespace-pre-wrap">
{note.content}
</p>
</div>
</div>
<div class="flex gap-1 shrink-0">
<button
type="button"
class="btn btn-ghost btn-xs tooltip tooltip-bottom"
phx-click="edit_note"
phx-value-uuid={note.uuid}
data-tip={gettext("Edit")}
>
<.icon name="hero-pencil" class="w-4 h-4 hidden sm:inline" />
<span class="sm:hidden whitespace-nowrap">{gettext("Edit")}</span>
</button>
<button
type="button"
class="btn btn-ghost btn-xs text-error tooltip tooltip-bottom"
phx-click="delete_note"
phx-value-uuid={note.uuid}
data-confirm={gettext("Are you sure you want to delete this note?")}
data-tip={gettext("Delete")}
>
<.icon name="hero-trash" class="w-4 h-4 hidden sm:inline" />
<span class="sm:hidden whitespace-nowrap">{gettext("Delete")}</span>
</button>
</div>
</div>
</div>
<% end %>
</div>
</div>
</div>
<% else %>
<div class="card bg-base-200 shadow-sm">
<div class="card-body items-center text-center py-12">
<.icon name="hero-document-text" class="w-12 h-12 text-base-content/30" />
<p class="text-base-content/50 mt-2">{gettext("No notes yet")}</p>
<p class="text-sm text-base-content/40">
{gettext("Add a note above to start documenting information about this user.")}
</p>
</div>
</div>
<% end %>
</div>
<% end %>
</div>
</div>
<%!-- Delete Confirmation Modal --%>
<%= if @show_delete_modal do %>
<div class="modal modal-open">
<div class="modal-box max-w-2xl">
<h3 class="font-bold text-lg text-error flex items-center gap-2">
<.icon name="hero-exclamation-triangle" class="w-6 h-6" />
{gettext("Delete User")}
</h3>
<div class="py-4 space-y-4">
<p class="text-base-content">
{gettext("Are you sure you want to permanently delete")}
<strong>{@user.email}</strong>? {gettext("This action cannot be undone.")}
</p>
<div class="bg-warning/10 border border-warning/30 rounded-lg p-4">
<h4 class="font-semibold text-warning mb-2">
{gettext("Data that will be permanently deleted:")}
</h4>
<ul class="list-disc list-inside text-sm text-base-content/80 space-y-1">
<li>{gettext("User account and profile information")}</li>
<li>{gettext("All session tokens and authentication data")}</li>
<li>{gettext("OAuth provider connections")}</li>
<li>{gettext("Billing profiles and payment methods")}</li>
<li>{gettext("Shopping carts and pending orders")}</li>
<li>{gettext("Admin notes about this user")}</li>
</ul>
</div>
<div class="bg-info/10 border border-info/30 rounded-lg p-4">
<h4 class="font-semibold text-info mb-2">
{gettext("Data that will be anonymized (preserved without PII):")}
</h4>
<ul class="list-disc list-inside text-sm text-base-content/80 space-y-1">
<li>{gettext("Orders - Financial records preserved, anonymized")}</li>
<li>{gettext("Posts - Content preserved, marked as deleted author")}</li>
<li>{gettext("Comments - Content preserved, marked as deleted author")}</li>
<li>{gettext("Support tickets - History preserved, anonymized")}</li>
<li>{gettext("Email logs - Compliance records retained, anonymized")}</li>
<li>{gettext("Files - Content preserved, ownership removed")}</li>
</ul>
</div>
</div>
<div class="modal-action">
<button type="button" class="btn btn-error" phx-click="delete_user">
<.icon name="hero-trash" class="w-4 h-4 mr-2" />
{gettext("Delete User")}
</button>
<button type="button" class="btn btn-outline" phx-click="hide_delete_modal">
{gettext("Cancel")}
</button>
</div>
</div>
<div class="modal-backdrop" phx-click="hide_delete_modal"></div>
</div>
<% end %>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>