623 lines
26 KiB
PHP
623 lines
26 KiB
PHP
@extends('admin.layout')
|
||
|
||
@section('title', 'Users')
|
||
|
||
@php
|
||
$adminNeedsOtp = auth()->user()->two_factor_enabled &&
|
||
(! session('admin_2fa_verified_at') || now()->timestamp - session('admin_2fa_verified_at') >= 1800);
|
||
|
||
$totalUsers = \App\Models\User::count();
|
||
$totalAdmins = \App\Models\User::whereIn('role', ['admin','super_admin'])->count();
|
||
$newThisWeek = \App\Models\User::where('created_at', '>=', now()->subDays(7))->count();
|
||
$unverified = \App\Models\User::whereNull('email_verified_at')->count();
|
||
@endphp
|
||
|
||
@section('extra_styles')
|
||
<style>
|
||
|
||
/* ═══ STAT CARDS ═══════════════════════════════════════════════════ */
|
||
.u-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 16px;
|
||
margin-bottom: 24px;
|
||
}
|
||
.u-stat {
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 14px;
|
||
padding: 20px 22px;
|
||
position: relative;
|
||
overflow: hidden;
|
||
transition: border-color .2s, transform .2s;
|
||
}
|
||
.u-stat:hover { border-color: #3a3a3a; transform: translateY(-1px); }
|
||
.u-stat-top { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 14px; }
|
||
.u-stat-icon {
|
||
width: 40px; height: 40px; border-radius: 10px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 17px; flex-shrink: 0;
|
||
}
|
||
.u-stat-icon.c-blue { background: rgba(96,165,250,.14); color: #60a5fa; }
|
||
.u-stat-icon.c-red { background: rgba(248,113,113,.14); color: #f87171; }
|
||
.u-stat-icon.c-green { background: rgba(52,211,153,.14); color: #34d399; }
|
||
.u-stat-icon.c-amber { background: rgba(251,191,36,.14); color: #fbbf24; }
|
||
.u-stat-accent {
|
||
position: absolute; top: 0; left: 0; right: 0; height: 2px;
|
||
border-radius: 14px 14px 0 0;
|
||
}
|
||
.u-stat-val { font-size: 32px; font-weight: 700; line-height: 1; color: var(--text-primary); letter-spacing: -1px; }
|
||
.u-stat-lbl { font-size: 13px; color: var(--text-secondary); margin-top: 4px; font-weight: 500; }
|
||
|
||
/* ═══ FILTER BAR ══════════════════════════════════════════════════ */
|
||
.u-filter {
|
||
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
||
padding: 14px 18px;
|
||
}
|
||
.u-search {
|
||
position: relative; flex: 1; min-width: 200px;
|
||
}
|
||
.u-search i {
|
||
position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
|
||
font-size: 13px; color: var(--text-secondary); pointer-events: none;
|
||
}
|
||
.u-search input {
|
||
width: 100%; height: 38px;
|
||
background: var(--bg-card2, #1a1a1a);
|
||
border: 1px solid var(--border-color); border-radius: 9px;
|
||
padding: 0 14px 0 36px; color: var(--text-primary); font-size: 13px;
|
||
transition: border-color .15s;
|
||
}
|
||
.u-search input:focus { outline: none; border-color: var(--brand-red); }
|
||
.u-search input::placeholder { color: #444; }
|
||
|
||
/* ═══ TABLE ════════════════════════════════════════════════════════ */
|
||
.u-table-wrap { overflow-x: auto; }
|
||
|
||
.u-table {
|
||
width: 100%; border-collapse: collapse; min-width: 640px;
|
||
}
|
||
|
||
.u-table thead tr {
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
.u-table thead th {
|
||
padding: 11px 18px;
|
||
font-size: 11px; font-weight: 600; letter-spacing: .7px;
|
||
text-transform: uppercase; color: var(--text-secondary);
|
||
white-space: nowrap; text-align: left;
|
||
}
|
||
.u-table thead th.right { text-align: right; }
|
||
|
||
.u-table tbody tr {
|
||
border-bottom: 1px solid rgba(255,255,255,.04);
|
||
cursor: pointer;
|
||
transition: background .1s;
|
||
}
|
||
.u-table tbody tr:last-child { border-bottom: none; }
|
||
.u-table tbody tr:hover { background: rgba(255,255,255,.03); }
|
||
.u-table tbody td {
|
||
padding: 0 18px; height: 68px;
|
||
vertical-align: middle; font-size: 13px;
|
||
}
|
||
|
||
/* Identity cell */
|
||
.u-identity { display: flex; align-items: center; gap: 13px; }
|
||
.u-avatar-wrap { position: relative; flex-shrink: 0; }
|
||
.u-avatar {
|
||
width: 40px; height: 40px; border-radius: 50%;
|
||
object-fit: cover; display: block;
|
||
border: 2px solid var(--border-color);
|
||
transition: border-color .15s;
|
||
}
|
||
.u-table tbody tr:hover .u-avatar { border-color: var(--brand-red); }
|
||
.u-name {
|
||
font-size: 14px; font-weight: 600; color: var(--text-primary);
|
||
white-space: nowrap; display: flex; align-items: center; gap: 7px;
|
||
line-height: 1;
|
||
}
|
||
.u-email { font-size: 12px; color: #555; margin-top: 3px; }
|
||
.u-you {
|
||
font-size: 9px; font-weight: 700; letter-spacing: .5px;
|
||
padding: 2px 6px; border-radius: 4px;
|
||
background: rgba(230,30,30,.15); color: var(--brand-red);
|
||
text-transform: uppercase; flex-shrink: 0;
|
||
}
|
||
|
||
/* Role cell */
|
||
.u-role { display: flex; align-items: center; gap: 7px; font-size: 13px; font-weight: 500; white-space: nowrap; }
|
||
.u-role-dot {
|
||
width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
|
||
}
|
||
.u-role-dot.super { background: #f87171; box-shadow: 0 0 6px rgba(248,113,113,.5); }
|
||
.u-role-dot.admin { background: #fbbf24; box-shadow: 0 0 6px rgba(251,191,36,.4); }
|
||
.u-role-dot.user { background: #555; }
|
||
|
||
/* Status cell */
|
||
.u-status { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 500; white-space: nowrap; }
|
||
.u-status-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
|
||
.u-status.verified { color: #34d399; }
|
||
.u-status.verified .u-status-dot { background: #34d399; box-shadow: 0 0 5px rgba(52,211,153,.5); }
|
||
.u-status.unverified { color: #fbbf24; }
|
||
.u-status.unverified .u-status-dot { background: #fbbf24; }
|
||
|
||
/* Videos cell */
|
||
.u-vcount { font-size: 15px; font-weight: 700; color: var(--text-primary); }
|
||
.u-vcount-lbl { font-size: 11px; color: #444; }
|
||
|
||
/* Joined cell */
|
||
.u-joined-date { font-size: 13px; font-weight: 500; color: var(--text-primary); }
|
||
.u-joined-ago { font-size: 11px; color: #444; margin-top: 2px; }
|
||
|
||
/* Row click hint */
|
||
.u-table tbody tr:hover td:last-child::after {
|
||
content: '⋯';
|
||
float: right;
|
||
color: #444;
|
||
font-size: 18px;
|
||
line-height: 1;
|
||
}
|
||
|
||
/* Row dropdown */
|
||
#uRowMenu {
|
||
display: none;
|
||
position: fixed;
|
||
z-index: 9999;
|
||
min-width: 190px;
|
||
background: #1c1c1c;
|
||
border: 1px solid #2e2e2e;
|
||
border-radius: 10px;
|
||
padding: 5px 0;
|
||
box-shadow: 0 12px 40px rgba(0,0,0,.7);
|
||
animation: uMenuIn .1s ease;
|
||
}
|
||
@keyframes uMenuIn {
|
||
from { opacity:0; transform:translateY(-4px); }
|
||
to { opacity:1; transform:translateY(0); }
|
||
}
|
||
#uRowMenu .u-menu-item {
|
||
display: flex; align-items: center; gap: 10px;
|
||
width: 100%; padding: 9px 14px;
|
||
background: none; border: none;
|
||
color: var(--text-primary); font-size: 13px;
|
||
cursor: pointer; text-align: left; text-decoration: none;
|
||
transition: background .1s;
|
||
white-space: nowrap;
|
||
}
|
||
#uRowMenu .u-menu-item:hover { background: rgba(255,255,255,.06); }
|
||
#uRowMenu .u-menu-item.danger { color: #f87171; }
|
||
#uRowMenu .u-menu-item.danger:hover { background: rgba(248,113,113,.1); }
|
||
#uRowMenu .u-menu-sep {
|
||
height: 1px; background: #2a2a2a; margin: 4px 0;
|
||
}
|
||
|
||
/* Empty state */
|
||
.u-empty { padding: 80px 20px; text-align: center; }
|
||
.u-empty-circle {
|
||
width: 72px; height: 72px; border-radius: 50%;
|
||
background: rgba(255,255,255,.03); border: 1px solid var(--border-color);
|
||
margin: 0 auto 18px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 30px; color: #333;
|
||
}
|
||
.u-empty h3 { font-size: 16px; font-weight: 600; color: var(--text-secondary); margin: 0 0 6px; }
|
||
.u-empty p { font-size: 13px; color: #444; margin: 0; }
|
||
|
||
/* ═══ RESPONSIVE ════════════════════════════════════════════════════ */
|
||
@media (max-width: 960px) {
|
||
.u-stats { grid-template-columns: repeat(2, 1fr); }
|
||
.u-hide-md { display: none !important; }
|
||
}
|
||
@media (max-width: 600px) {
|
||
.u-stats { grid-template-columns: repeat(2, 1fr); }
|
||
.u-hide-sm { display: none !important; }
|
||
}
|
||
</style>
|
||
@endsection
|
||
|
||
@section('content')
|
||
|
||
<div class="adm-page-header">
|
||
<h1 class="adm-page-title"><i class="bi bi-people"></i> Users</h1>
|
||
<a href="{{ route('admin.dashboard') }}" class="adm-btn" style="font-size:12px;">
|
||
<i class="bi bi-arrow-left"></i> Dashboard
|
||
</a>
|
||
</div>
|
||
|
||
{{-- Alerts --}}
|
||
@if(session('success'))
|
||
<div class="adm-alert adm-alert-success">
|
||
<i class="bi bi-check-circle-fill"></i>
|
||
<span>{{ session('success') }}</span>
|
||
<button class="adm-alert-close"><i class="bi bi-x"></i></button>
|
||
</div>
|
||
@endif
|
||
@if(session('error'))
|
||
<div class="adm-alert adm-alert-error">
|
||
<i class="bi bi-exclamation-circle-fill"></i>
|
||
<span>{{ session('error') }}</span>
|
||
<button class="adm-alert-close"><i class="bi bi-x"></i></button>
|
||
</div>
|
||
@endif
|
||
|
||
{{-- Stats --}}
|
||
<div class="u-stats">
|
||
<div class="u-stat">
|
||
<div class="u-stat-accent" style="background: linear-gradient(90deg,#60a5fa,#3b82f6);"></div>
|
||
<div class="u-stat-top">
|
||
<div>
|
||
<div class="u-stat-val">{{ number_format($totalUsers) }}</div>
|
||
<div class="u-stat-lbl">Total users</div>
|
||
</div>
|
||
<div class="u-stat-icon c-blue"><i class="bi bi-people-fill"></i></div>
|
||
</div>
|
||
</div>
|
||
<div class="u-stat">
|
||
<div class="u-stat-accent" style="background: linear-gradient(90deg,#f87171,#e61e1e);"></div>
|
||
<div class="u-stat-top">
|
||
<div>
|
||
<div class="u-stat-val">{{ $totalAdmins }}</div>
|
||
<div class="u-stat-lbl">Admins</div>
|
||
</div>
|
||
<div class="u-stat-icon c-red"><i class="bi bi-shield-fill"></i></div>
|
||
</div>
|
||
</div>
|
||
<div class="u-stat">
|
||
<div class="u-stat-accent" style="background: linear-gradient(90deg,#34d399,#059669);"></div>
|
||
<div class="u-stat-top">
|
||
<div>
|
||
<div class="u-stat-val">{{ $newThisWeek }}</div>
|
||
<div class="u-stat-lbl">New this week</div>
|
||
</div>
|
||
<div class="u-stat-icon c-green"><i class="bi bi-person-plus-fill"></i></div>
|
||
</div>
|
||
</div>
|
||
<div class="u-stat">
|
||
<div class="u-stat-accent" style="background: linear-gradient(90deg,#fbbf24,#d97706);"></div>
|
||
<div class="u-stat-top">
|
||
<div>
|
||
<div class="u-stat-val">{{ $unverified }}</div>
|
||
<div class="u-stat-lbl">Unverified</div>
|
||
</div>
|
||
<div class="u-stat-icon c-amber"><i class="bi bi-envelope-exclamation-fill"></i></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Filter --}}
|
||
<div class="adm-card" style="margin-bottom:16px;">
|
||
<form method="GET" action="{{ route('admin.users') }}" class="u-filter">
|
||
<div class="u-search">
|
||
<i class="bi bi-search"></i>
|
||
<input type="text" name="search" placeholder="Search by name or email…"
|
||
value="{{ request('search') }}" autocomplete="off">
|
||
</div>
|
||
<select name="role" class="adm-select">
|
||
<option value="">All Roles</option>
|
||
<option value="user" {{ request('role') === 'user' ? 'selected' : '' }}>Users</option>
|
||
<option value="admin" {{ request('role') === 'admin' ? 'selected' : '' }}>Admins</option>
|
||
<option value="super_admin" {{ request('role') === 'super_admin' ? 'selected' : '' }}>Super Admins</option>
|
||
</select>
|
||
<select name="sort" class="adm-select">
|
||
<option value="latest" {{ request('sort','latest') === 'latest' ? 'selected' : '' }}>Newest first</option>
|
||
<option value="oldest" {{ request('sort') === 'oldest' ? 'selected' : '' }}>Oldest first</option>
|
||
<option value="name_asc" {{ request('sort') === 'name_asc' ? 'selected' : '' }}>Name A–Z</option>
|
||
<option value="name_desc" {{ request('sort') === 'name_desc' ? 'selected' : '' }}>Name Z–A</option>
|
||
</select>
|
||
<button type="submit" class="adm-btn adm-btn-primary"><i class="bi bi-funnel-fill"></i> Filter</button>
|
||
@if(request()->hasAny(['search','role','sort']))
|
||
<a href="{{ route('admin.users') }}" class="adm-btn"><i class="bi bi-x-lg"></i> Clear</a>
|
||
@endif
|
||
</form>
|
||
</div>
|
||
|
||
{{-- Table --}}
|
||
<div class="adm-card">
|
||
<div class="adm-card-header">
|
||
<div class="adm-card-title">
|
||
<i class="bi bi-people"></i> Members
|
||
<span class="adm-badge adm-badge-user">{{ $users->total() ?? $users->count() }}</span>
|
||
</div>
|
||
<span style="font-size:12px; color:#444; display:flex; align-items:center; gap:5px;">
|
||
<i class="bi bi-hand-index" style="font-size:11px;"></i>
|
||
Click a row for actions
|
||
</span>
|
||
</div>
|
||
|
||
<div class="u-table-wrap">
|
||
<table class="u-table">
|
||
<thead>
|
||
<tr>
|
||
<th>User</th>
|
||
<th class="u-hide-sm">Role</th>
|
||
<th class="u-hide-sm">Status</th>
|
||
<th class="u-hide-md">Videos</th>
|
||
<th class="u-hide-md">Likes</th>
|
||
<th class="u-hide-md">Shares</th>
|
||
<th class="u-hide-md">Joined</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@forelse($users as $user)
|
||
<tr class="u-clickable-row"
|
||
data-channel="{{ route('channel', $user->channel) }}"
|
||
data-user-id="{{ $user->id }}"
|
||
data-user-name="{{ addslashes($user->name) }}"
|
||
data-verified="{{ $user->email_verified_at ? '1' : '0' }}"
|
||
data-is-self="{{ $user->id === auth()->id() ? '1' : '0' }}"
|
||
data-is-super="{{ $user->isSuperAdmin() ? '1' : '0' }}"
|
||
data-edit-url="{{ route('admin.users.edit', $user->id) }}"
|
||
data-verify-url="{{ route('admin.users.verify', $user->id) }}"
|
||
data-impersonate-url="{{ route('admin.users.impersonate', $user->id) }}"
|
||
data-delete-id="{{ $user->id }}">
|
||
|
||
{{-- Identity --}}
|
||
<td>
|
||
<div class="u-identity">
|
||
<div class="u-avatar-wrap">
|
||
<img class="u-avatar" src="{{ $user->avatar_url }}" alt="{{ $user->name }}">
|
||
</div>
|
||
<div>
|
||
<div class="u-name">
|
||
{{ $user->name }}
|
||
@if($user->id === auth()->id())
|
||
<span class="u-you">you</span>
|
||
@endif
|
||
</div>
|
||
<div class="u-email">{{ $user->email }}</div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
|
||
{{-- Role --}}
|
||
<td class="u-hide-sm">
|
||
@if($user->role === 'super_admin')
|
||
<div class="u-role"><span class="u-role-dot super"></span> Super Admin</div>
|
||
@elseif($user->role === 'admin')
|
||
<div class="u-role"><span class="u-role-dot admin"></span> Admin</div>
|
||
@else
|
||
<div class="u-role" style="color:#666;"><span class="u-role-dot user"></span> User</div>
|
||
@endif
|
||
</td>
|
||
|
||
{{-- Status --}}
|
||
<td class="u-hide-sm">
|
||
@if($user->email_verified_at)
|
||
<div class="u-status verified">
|
||
<span class="u-status-dot"></span> Verified
|
||
</div>
|
||
@else
|
||
<div class="u-status unverified">
|
||
<span class="u-status-dot"></span> Pending
|
||
</div>
|
||
@endif
|
||
</td>
|
||
|
||
{{-- Videos --}}
|
||
<td class="u-hide-md">
|
||
<div class="u-vcount">{{ $user->videos->count() }}</div>
|
||
<div class="u-vcount-lbl">videos</div>
|
||
</td>
|
||
|
||
{{-- Likes --}}
|
||
<td class="u-hide-md">
|
||
@php $videoIds = $user->videos->pluck('id'); @endphp
|
||
<div class="u-vcount">{{ number_format(\DB::table('video_likes')->whereIn('video_id', $videoIds)->count()) }}</div>
|
||
<div class="u-vcount-lbl">likes</div>
|
||
</td>
|
||
|
||
{{-- Shares --}}
|
||
<td class="u-hide-md">
|
||
<div class="u-vcount">{{ number_format(\DB::table('video_shares')->whereIn('video_id', $videoIds)->count()) }}</div>
|
||
<div class="u-vcount-lbl">shares</div>
|
||
</td>
|
||
|
||
{{-- Joined --}}
|
||
<td class="u-hide-md">
|
||
<div class="u-joined-date">{{ $user->created_at->format('M d, Y') }}</div>
|
||
<div class="u-joined-ago">{{ $user->created_at->diffForHumans() }}</div>
|
||
</td>
|
||
|
||
|
||
</tr>
|
||
@empty
|
||
<tr>
|
||
<td colspan="7" style="border:none;">
|
||
<div class="u-empty">
|
||
<div class="u-empty-circle"><i class="bi bi-people"></i></div>
|
||
<h3>No users found</h3>
|
||
<p>Try adjusting your search or filter criteria</p>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
@endforelse
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
@if($users instanceof \Illuminate\Pagination\LengthAwarePaginator && $users->hasPages())
|
||
<div style="padding:14px 20px; border-top:1px solid var(--border-color);">
|
||
{{ $users->onEachSide(1)->links() }}
|
||
</div>
|
||
@endif
|
||
</div>
|
||
|
||
{{-- Row action dropdown --}}
|
||
<div id="uRowMenu"></div>
|
||
|
||
{{-- Delete dialog --}}
|
||
<div class="adm-dialog-overlay" id="deleteDialog">
|
||
<div class="adm-dialog">
|
||
<div class="adm-dialog-header">
|
||
<div class="adm-dialog-title">
|
||
<i class="bi bi-exclamation-triangle-fill"></i> Delete User
|
||
</div>
|
||
<button type="button" class="adm-btn adm-btn-sm" onclick="closeDeleteDialog()"
|
||
style="border:none;background:none;color:var(--text-secondary);">
|
||
<i class="bi bi-x-lg"></i>
|
||
</button>
|
||
</div>
|
||
<div class="adm-dialog-body">
|
||
<p>You are about to permanently delete <strong id="dlgUserName"></strong>.</p>
|
||
<div class="adm-dialog-warning">
|
||
<i class="bi bi-info-circle-fill" style="flex-shrink:0;margin-top:1px;"></i>
|
||
All videos uploaded by this user will also be deleted. This cannot be undone.
|
||
</div>
|
||
@if($adminNeedsOtp)
|
||
<div style="margin-top:16px;">
|
||
<label style="font-size:13px;color:var(--text-secondary);display:flex;align-items:center;gap:6px;margin-bottom:8px;">
|
||
<i class="bi bi-shield-lock-fill" style="color:#a78bfa;"></i>
|
||
Enter your <strong style="color:var(--text-primary);">2FA code</strong> to confirm
|
||
</label>
|
||
<input type="text" id="dlgUserOtp" inputmode="numeric" pattern="[0-9]*"
|
||
maxlength="6" autocomplete="one-time-code" placeholder="000 000"
|
||
style="width:100%;background:#111;border:1px solid var(--border-color);border-radius:8px;padding:12px 16px;color:#fff;font-size:26px;letter-spacing:.25em;text-align:center;transition:border-color .15s;"
|
||
onfocus="this.style.borderColor='#a78bfa'" onblur="this.style.borderColor='var(--border-color)'">
|
||
</div>
|
||
@endif
|
||
<div id="dlgUserError" style="display:none;margin-top:10px;padding:10px 14px;background:rgba(248,113,113,.1);border:1px solid rgba(248,113,113,.25);border-radius:8px;color:#f87171;font-size:13px;"></div>
|
||
</div>
|
||
<div class="adm-dialog-footer">
|
||
<button type="button" class="adm-btn" onclick="closeDeleteDialog()">Cancel</button>
|
||
<button type="button" class="adm-btn adm-btn-danger" id="dlgUserConfirmBtn" onclick="confirmDeleteUser()"
|
||
style="height:36px;">
|
||
<i class="bi bi-trash"></i> Delete permanently
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@endsection
|
||
|
||
@section('scripts')
|
||
<script>
|
||
const _adminNeedsOtp = {{ $adminNeedsOtp ? 'true' : 'false' }};
|
||
let _deleteUserId = null;
|
||
|
||
/* ── Row dropdown ─────────────────────────────────────────────── */
|
||
const _menu = document.getElementById('uRowMenu');
|
||
|
||
function _closeMenu() { _menu.style.display = 'none'; }
|
||
|
||
document.querySelectorAll('.u-clickable-row').forEach(function(row) {
|
||
row.addEventListener('click', function(e) {
|
||
e.stopPropagation();
|
||
const d = row.dataset;
|
||
const isSelf = d.isSelf === '1';
|
||
const isSuper = d.isSuper === '1';
|
||
const verified = d.verified === '1';
|
||
|
||
let html = '';
|
||
|
||
// Open channel
|
||
html += `<a class="u-menu-item" href="${d.channel}" target="_blank"><i class="bi bi-box-arrow-up-right"></i> Open Channel</a>`;
|
||
html += `<a class="u-menu-item" href="${d.editUrl}"><i class="bi bi-pencil"></i> Edit User</a>`;
|
||
|
||
if (!verified) {
|
||
html += `<button class="u-menu-item" onclick="_verifyUser('${d.verifyUrl}')"><i class="bi bi-patch-check-fill" style="color:#34d399;"></i> Verify Account</button>`;
|
||
}
|
||
|
||
if (!isSelf && !isSuper) {
|
||
html += `<button class="u-menu-item" onclick="_impersonateUser('${d.impersonateUrl}')"><i class="bi bi-person-fill-gear" style="color:#60a5fa;"></i> Impersonate</button>`;
|
||
}
|
||
|
||
if (!isSelf) {
|
||
html += `<div class="u-menu-sep"></div>`;
|
||
html += `<button class="u-menu-item danger" onclick="_closeMenu(); openDeleteDialog(${d.deleteId}, '${d.userName}')"><i class="bi bi-trash"></i> Delete User</button>`;
|
||
}
|
||
|
||
_menu.innerHTML = html;
|
||
|
||
// Position near click, keep within viewport
|
||
const vw = window.innerWidth, vh = window.innerHeight;
|
||
const mw = 200, mh = _menu.childElementCount * 40;
|
||
let x = e.clientX, y = e.clientY + 6;
|
||
if (x + mw > vw - 12) x = vw - mw - 12;
|
||
if (y + mh > vh - 12) y = e.clientY - mh - 6;
|
||
|
||
_menu.style.left = x + 'px';
|
||
_menu.style.top = y + 'px';
|
||
_menu.style.display = 'block';
|
||
});
|
||
});
|
||
|
||
document.addEventListener('click', _closeMenu);
|
||
document.addEventListener('keydown', e => { if (e.key === 'Escape') { _closeMenu(); closeDeleteDialog(); } });
|
||
|
||
function _verifyUser(url) {
|
||
_closeMenu();
|
||
const f = document.createElement('form');
|
||
f.method = 'POST'; f.action = url;
|
||
f.innerHTML = '<input type="hidden" name="_token" value="{{ csrf_token() }}">';
|
||
document.body.appendChild(f); f.submit();
|
||
}
|
||
function _impersonateUser(url) {
|
||
_closeMenu();
|
||
const f = document.createElement('form');
|
||
f.method = 'POST'; f.action = url;
|
||
f.innerHTML = '<input type="hidden" name="_token" value="{{ csrf_token() }}">';
|
||
document.body.appendChild(f); f.submit();
|
||
}
|
||
|
||
/* ── Delete dialog ────────────────────────────────────────────── */
|
||
function openDeleteDialog(userId, userName) {
|
||
_deleteUserId = userId;
|
||
document.getElementById('dlgUserName').textContent = userName;
|
||
document.getElementById('dlgUserError').style.display = 'none';
|
||
if (_adminNeedsOtp) document.getElementById('dlgUserOtp').value = '';
|
||
document.getElementById('deleteDialog').classList.add('open');
|
||
if (_adminNeedsOtp) setTimeout(() => document.getElementById('dlgUserOtp').focus(), 120);
|
||
}
|
||
function closeDeleteDialog() {
|
||
document.getElementById('deleteDialog').classList.remove('open');
|
||
_deleteUserId = null;
|
||
}
|
||
function confirmDeleteUser() {
|
||
if (!_deleteUserId) return;
|
||
const btn = document.getElementById('dlgUserConfirmBtn');
|
||
const errEl = document.getElementById('dlgUserError');
|
||
const otp = _adminNeedsOtp ? document.getElementById('dlgUserOtp').value.replace(/\s/g,'') : '';
|
||
|
||
if (_adminNeedsOtp && otp.length !== 6) {
|
||
errEl.textContent = 'Please enter your 6-digit 2FA code.';
|
||
errEl.style.display = 'block';
|
||
document.getElementById('dlgUserOtp').focus();
|
||
return;
|
||
}
|
||
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Deleting…';
|
||
errEl.style.display = 'none';
|
||
|
||
const body = new URLSearchParams({ _token: '{{ csrf_token() }}', _method: 'DELETE' });
|
||
if (_adminNeedsOtp) body.append('otp_code', otp);
|
||
|
||
fetch('/admin/users/' + _deleteUserId, {
|
||
method: 'POST',
|
||
headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: body.toString()
|
||
})
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
if (data.success) { closeDeleteDialog(); window.location.reload(); }
|
||
else {
|
||
errEl.textContent = data.message || 'Delete failed.';
|
||
errEl.style.display = 'block';
|
||
btn.disabled = false;
|
||
btn.innerHTML = '<i class="bi bi-trash"></i> Delete permanently';
|
||
if (_adminNeedsOtp) { document.getElementById('dlgUserOtp').value = ''; document.getElementById('dlgUserOtp').focus(); }
|
||
}
|
||
})
|
||
.catch(() => {
|
||
errEl.textContent = 'An unexpected error occurred.';
|
||
errEl.style.display = 'block';
|
||
btn.disabled = false;
|
||
btn.innerHTML = '<i class="bi bi-trash"></i> Delete permanently';
|
||
});
|
||
}
|
||
document.getElementById('deleteDialog').addEventListener('click', e => {
|
||
if (e.target === document.getElementById('deleteDialog')) closeDeleteDialog();
|
||
});
|
||
</script>
|
||
@endsection
|