SurveyVista/Web/Areas/Admin/Views/Roles/Index.cshtml
2026-03-07 02:37:33 +01:00

746 lines
41 KiB
Text

@* Views/Admin/Roles/Index.cshtml *@
@model IEnumerable<RoleViewModel>
@using Web.ViewModel.AccountVM
@using Web.Authorization
@{
ViewData["Title"] = "Role Management";
var permissionGroups = ViewBag.PermissionGroups as Dictionary<string, List<PermissionItem>>;
}
@section Styles {
<style>
@@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
:root {
--nex-orange: #fb923c;
--nex-amber: #fbbf24;
--nex-yellow: #facc15;
--nex-blue: #60a5fa;
--nex-cyan: #22d3ee;
--nex-purple: #c084fc;
--nex-red: #f87171;
--nex-emerald: #34d399;
--nex-teal: #14b8a6;
--dark-900: #0f172a;
--dark-800: #1e293b;
--dark-700: #334155;
--dark-600: #475569;
--dark-500: #64748b;
--dark-400: #94a3b8;
--dark-300: #cbd5e1;
--glass-bg: rgba(255,255,255,0.04);
--glass-border: rgba(255,255,255,0.08);
--font-main: 'Space Grotesk', sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:var(--font-main); background:var(--dark-900); color:#e2e8f0; overflow-x:hidden; }
.nex-bg { position:fixed; inset:0; z-index:-1; overflow:hidden; }
.nex-bg .grid { position:absolute; inset:0; background-image:linear-gradient(rgba(251,146,60,0.04) 1px,transparent 1px),linear-gradient(90deg,rgba(251,146,60,0.04) 1px,transparent 1px); background-size:60px 60px; }
.nex-bg .mesh { position:absolute; inset:0; background:radial-gradient(circle at 15% 25%,rgba(251,146,60,0.08) 0%,transparent 50%),radial-gradient(circle at 85% 55%,rgba(192,132,252,0.06) 0%,transparent 50%),radial-gradient(circle at 50% 85%,rgba(96,165,250,0.05) 0%,transparent 50%); }
.page-header { position:relative; z-index:10; padding:2.5rem 0 1.5rem; }
.container { max-width:1400px; margin:0 auto; padding:0 2rem; }
.breadcrumb-nex { display:flex; align-items:center; gap:0.75rem; font-family:var(--font-mono); font-size:0.72rem; text-transform:uppercase; letter-spacing:0.06em; margin-bottom:1.5rem; flex-wrap:wrap; }
.breadcrumb-nex a { color:var(--dark-400); text-decoration:none; transition:color .2s; }
.breadcrumb-nex a:hover { color:var(--nex-orange); }
.breadcrumb-nex .sep { color:var(--dark-600); font-size:0.55rem; }
.breadcrumb-nex .current { color:var(--nex-orange); }
.header-row { display:flex; align-items:flex-start; justify-content:space-between; gap:2rem; flex-wrap:wrap; }
.header-badge { display:inline-flex; align-items:center; gap:0.5rem; padding:0.4rem 1rem; background:rgba(251,146,60,0.1); border:1px solid rgba(251,146,60,0.25); border-radius:50px; font-family:var(--font-mono); font-size:0.65rem; font-weight:600; color:var(--nex-orange); letter-spacing:0.1em; margin-bottom:1rem; }
.header-badge .dot { width:6px; height:6px; border-radius:50%; background:var(--nex-orange); animation:pulse-dot 2s ease infinite; }
@@keyframes pulse-dot { 0%,100%{opacity:1} 50%{opacity:.4} }
.header-title { font-size:2.2rem; font-weight:700; color:#f8fafc; line-height:1.2; margin-bottom:0.5rem; }
.header-title .grad { background:linear-gradient(135deg, var(--nex-orange), var(--nex-amber)); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
.header-sub { color:var(--dark-400); font-size:0.9rem; }
.header-actions { display:flex; gap:0.75rem; flex-wrap:wrap; padding-top:0.5rem; }
.h-btn { display:inline-flex; align-items:center; gap:0.5rem; padding:0.6rem 1.3rem; border-radius:10px; font-family:var(--font-main); font-size:0.82rem; font-weight:600; border:none; cursor:pointer; transition:all .25s; text-decoration:none; }
.h-btn.pri { background:linear-gradient(135deg, var(--nex-orange), var(--nex-amber)); color:#fff; }
.h-btn.pri:hover { transform:translateY(-2px); box-shadow:0 8px 25px rgba(251,146,60,0.3); }
.h-btn.sec { background:var(--glass-bg); border:1px solid var(--glass-border); color:#e2e8f0; }
.h-btn.sec:hover { background:rgba(251,146,60,0.1); border-color:rgba(251,146,60,0.3); color:var(--nex-orange); }
.h-btn.danger { background:rgba(248,113,113,0.15); border:1px solid rgba(248,113,113,0.3); color:var(--nex-red); }
.h-btn.danger:hover { background:rgba(248,113,113,0.25); transform:translateY(-2px); }
.h-btn:disabled { opacity:0.5; cursor:not-allowed; transform:none !important; }
/* Stats */
.stats-row { display:grid; grid-template-columns:repeat(3,1fr); gap:1rem; margin-bottom:2rem; }
.stat-chip { background:var(--glass-bg); border:1px solid var(--glass-border); border-radius:14px; padding:1.2rem 1.5rem; display:flex; align-items:center; gap:1rem; transition:all .3s; }
.stat-chip:hover { border-color:rgba(251,146,60,0.2); transform:translateY(-2px); }
.stat-chip .s-icon { width:44px; height:44px; border-radius:12px; display:flex; align-items:center; justify-content:center; font-size:1.1rem; flex-shrink:0; }
.stat-chip .s-icon.orange { background:rgba(251,146,60,0.15); color:var(--nex-orange); }
.stat-chip .s-icon.purple { background:rgba(192,132,252,0.15); color:var(--nex-purple); }
.stat-chip .s-icon.blue { background:rgba(96,165,250,0.15); color:var(--nex-blue); }
.stat-chip .s-val { font-size:1.6rem; font-weight:700; color:#f8fafc; line-height:1; }
.stat-chip .s-lbl { font-size:0.72rem; color:var(--dark-400); text-transform:uppercase; letter-spacing:0.05em; font-family:var(--font-mono); margin-top:2px; }
/* Card */
.nex-card { background:var(--glass-bg); border:1px solid var(--glass-border); border-radius:16px; backdrop-filter:blur(12px); position:relative; overflow:hidden; }
.nex-card .top-glow { position:absolute; top:0; left:0; right:0; height:2px; background:linear-gradient(90deg,transparent,var(--nex-orange),transparent); opacity:0.6; }
.tbl-head { display:flex; align-items:center; justify-content:space-between; padding:1.25rem 1.5rem; border-bottom:1px solid var(--glass-border); flex-wrap:wrap; gap:1rem; }
.tbl-title { display:flex; align-items:center; gap:0.75rem; font-size:1rem; font-weight:600; color:#f8fafc; }
.tbl-title i { color:var(--nex-orange); }
.tbl-count { font-family:var(--font-mono); font-size:0.7rem; color:var(--dark-400); background:rgba(251,146,60,0.1); padding:0.25rem 0.65rem; border-radius:6px; border:1px solid rgba(251,146,60,0.2); }
/* Table */
.role-table { width:100%; border-collapse:collapse; }
.role-table thead th { padding:0.85rem 1.5rem; text-align:left; font-family:var(--font-mono); font-size:0.68rem; font-weight:600; color:var(--dark-400); text-transform:uppercase; letter-spacing:0.08em; border-bottom:1px solid var(--glass-border); background:rgba(255,255,255,0.02); }
.role-table thead th:first-child { width:50px; text-align:center; }
.role-table thead th:last-child { text-align:right; }
.role-table tbody tr { border-bottom:1px solid rgba(255,255,255,0.04); transition:background .2s; }
.role-table tbody tr:hover { background:rgba(251,146,60,0.04); }
.role-table tbody tr:last-child { border-bottom:none; }
.role-table tbody td { padding:0.9rem 1.5rem; font-size:0.88rem; vertical-align:middle; }
.role-table tbody td:first-child { text-align:center; }
.role-table tbody td:last-child { text-align:right; }
.nex-check { width:18px; height:18px; accent-color:var(--nex-orange); cursor:pointer; }
.role-icon { width:38px; height:38px; border-radius:10px; display:inline-flex; align-items:center; justify-content:center; font-size:1rem; margin-right:0.75rem; vertical-align:middle; }
.role-name { font-weight:600; color:#f1f5f9; }
/* Permission pills in table */
.perm-pill { display:inline-flex; align-items:center; gap:0.25rem; padding:0.2rem 0.55rem; border-radius:5px; font-size:0.65rem; font-weight:600; font-family:var(--font-mono); margin:0.15rem; background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.08); color:var(--dark-300); }
.perm-pill.has { background:rgba(52,211,153,0.1); border-color:rgba(52,211,153,0.2); color:var(--nex-emerald); }
.perm-more { font-size:0.7rem; color:var(--dark-400); cursor:pointer; padding:0.2rem 0.5rem; border-radius:5px; background:rgba(255,255,255,0.05); }
.perm-more:hover { color:var(--nex-orange); }
.act-btn { display:inline-flex; align-items:center; justify-content:center; width:34px; height:34px; border-radius:8px; border:1px solid var(--glass-border); background:var(--glass-bg); color:var(--dark-300); cursor:pointer; transition:all .2s; font-size:0.82rem; margin-left:0.35rem; }
.act-btn:hover { transform:translateY(-1px); }
.act-btn.edit:hover { background:rgba(96,165,250,0.15); border-color:rgba(96,165,250,0.3); color:var(--nex-blue); }
.act-btn.del:hover { background:rgba(248,113,113,0.15); border-color:rgba(248,113,113,0.3); color:var(--nex-red); }
.tbl-foot { display:flex; align-items:center; justify-content:space-between; padding:1rem 1.5rem; border-top:1px solid var(--glass-border); flex-wrap:wrap; gap:0.75rem; }
.selected-info { font-family:var(--font-mono); font-size:0.72rem; color:var(--dark-400); }
.selected-info span { color:var(--nex-orange); font-weight:600; }
/* Modal */
.modal-overlay { display:none; position:fixed; inset:0; z-index:9999; background:rgba(2,6,23,0.8); backdrop-filter:blur(8px); align-items:center; justify-content:center; padding:2rem; }
.modal-overlay.active { display:flex; animation:fadeIn .25s ease; }
@@keyframes fadeIn { from{opacity:0} to{opacity:1} }
.modal-box { background:var(--dark-800); border:1px solid var(--glass-border); border-radius:20px; width:100%; max-width:620px; max-height:90vh; overflow-y:auto; position:relative; animation:slideUp .3s ease; }
@@keyframes slideUp { from{opacity:0;transform:translateY(20px)} to{opacity:1;transform:translateY(0)} }
.modal-box .m-glow { position:absolute; top:0; left:0; right:0; height:3px; border-radius:20px 20px 0 0; }
.modal-box .m-glow.orange { background:linear-gradient(90deg,var(--nex-orange),var(--nex-amber)); }
.modal-box .m-glow.blue { background:linear-gradient(90deg,var(--nex-blue),var(--nex-cyan)); }
.modal-box .m-glow.red { background:linear-gradient(90deg,var(--nex-red),#dc2626); }
.m-header { display:flex; align-items:center; justify-content:space-between; padding:1.5rem 1.75rem 1rem; }
.m-header-left { display:flex; align-items:center; gap:0.85rem; }
.m-header-icon { width:42px; height:42px; border-radius:12px; display:flex; align-items:center; justify-content:center; font-size:1.1rem; }
.m-header-icon.orange { background:rgba(251,146,60,0.15); color:var(--nex-orange); }
.m-header-icon.blue { background:rgba(96,165,250,0.15); color:var(--nex-blue); }
.m-header-icon.red { background:rgba(248,113,113,0.15); color:var(--nex-red); }
.m-header h3 { font-size:1.1rem; font-weight:700; color:#f8fafc; }
.m-header p { font-size:0.78rem; color:var(--dark-400); margin-top:2px; }
.m-close { width:36px; height:36px; border-radius:10px; border:1px solid var(--glass-border); background:none; color:var(--dark-400); display:flex; align-items:center; justify-content:center; cursor:pointer; transition:all .2s; font-size:0.9rem; }
.m-close:hover { background:rgba(248,113,113,0.15); border-color:rgba(248,113,113,0.3); color:var(--nex-red); }
.m-body { padding:0.5rem 1.75rem 1.5rem; }
.f-group { margin-bottom:1.1rem; }
.f-label { display:block; font-family:var(--font-mono); font-size:0.7rem; font-weight:600; color:var(--dark-300); text-transform:uppercase; letter-spacing:0.06em; margin-bottom:0.45rem; }
.f-input { width:100%; padding:0.65rem 1rem; background:rgba(255,255,255,0.05); border:1px solid var(--glass-border); border-radius:10px; color:#e2e8f0; font-family:var(--font-main); font-size:0.88rem; outline:none; transition:border-color .2s, box-shadow .2s; }
.f-input:focus { border-color:rgba(251,146,60,0.5); box-shadow:0 0 0 3px rgba(251,146,60,0.1); }
.f-input::placeholder { color:var(--dark-500); }
/* Permission groups */
.perm-groups { max-height:350px; overflow-y:auto; padding-right:0.5rem; }
.perm-groups::-webkit-scrollbar { width:4px; }
.perm-groups::-webkit-scrollbar-track { background:transparent; }
.perm-groups::-webkit-scrollbar-thumb { background:var(--dark-600); border-radius:4px; }
.perm-group { margin-bottom:1rem; }
.perm-group-head { display:flex; align-items:center; justify-content:space-between; padding:0.5rem 0.75rem; background:rgba(255,255,255,0.03); border:1px solid var(--glass-border); border-radius:10px 10px 0 0; cursor:pointer; }
.perm-group-head h5 { font-size:0.78rem; font-weight:700; color:var(--dark-200); display:flex; align-items:center; gap:0.5rem; }
.perm-group-head h5 i { color:var(--nex-orange); font-size:0.75rem; }
.perm-group-toggle { font-size:0.65rem; color:var(--nex-orange); cursor:pointer; font-family:var(--font-mono); font-weight:600; background:none; border:none; padding:0.2rem 0.5rem; border-radius:4px; }
.perm-group-toggle:hover { background:rgba(251,146,60,0.1); }
.perm-group-body { border:1px solid var(--glass-border); border-top:none; border-radius:0 0 10px 10px; padding:0.6rem 0.75rem; display:flex; flex-wrap:wrap; gap:0.4rem; }
.perm-chip { position:relative; }
.perm-chip input { position:absolute; opacity:0; pointer-events:none; }
.perm-chip label { display:inline-flex; align-items:center; gap:0.35rem; padding:0.35rem 0.7rem; border-radius:7px; border:1px solid var(--glass-border); background:var(--glass-bg); color:var(--dark-400); font-size:0.72rem; font-weight:600; cursor:pointer; transition:all .2s; font-family:var(--font-mono); }
.perm-chip input:checked + label { background:rgba(251,146,60,0.15); border-color:rgba(251,146,60,0.4); color:var(--nex-orange); }
.perm-chip label:hover { border-color:rgba(251,146,60,0.3); }
.perm-chip label i { font-size:0.65rem; }
.m-footer { display:flex; align-items:center; justify-content:flex-end; gap:0.75rem; padding:1rem 1.75rem 1.5rem; border-top:1px solid var(--glass-border); }
.modal-errors { margin-bottom:1rem; padding:0.75rem 1rem; background:rgba(248,113,113,0.1); border:1px solid rgba(248,113,113,0.25); border-radius:10px; display:none; }
.modal-errors ul { list-style:none; padding:0; margin:0; }
.modal-errors li { font-size:0.8rem; color:var(--nex-red); padding:0.15rem 0; display:flex; align-items:center; gap:0.4rem; }
.modal-errors li i { font-size:0.7rem; }
.nex-toast { position:fixed; top:1.5rem; right:1.5rem; z-index:99999; padding:0.85rem 1.5rem; border-radius:12px; font-size:0.85rem; font-weight:600; display:flex; align-items:center; gap:0.6rem; animation:slideIn .3s ease,fadeOut .3s ease 2.7s forwards; pointer-events:none; }
.nex-toast.success { background:rgba(52,211,153,0.15); border:1px solid rgba(52,211,153,0.3); color:var(--nex-emerald); backdrop-filter:blur(12px); }
.nex-toast.error { background:rgba(248,113,113,0.15); border:1px solid rgba(248,113,113,0.3); color:var(--nex-red); backdrop-filter:blur(12px); }
@@keyframes slideIn { from{opacity:0;transform:translateX(30px)} to{opacity:1;transform:translateX(0)} }
@@keyframes fadeOut { to{opacity:0;transform:translateY(-10px)} }
.del-warning { font-size:0.82rem; color:var(--dark-300); line-height:1.6; }
.del-warning strong { color:var(--nex-red); }
.btn-spinner { display:none; width:16px; height:16px; border:2px solid rgba(255,255,255,0.3); border-top-color:#fff; border-radius:50%; animation:spin .6s linear infinite; }
@@keyframes spin { to{transform:rotate(360deg)} }
.empty-state { text-align:center; padding:4rem 2rem; }
.empty-state i { font-size:3rem; color:var(--dark-600); margin-bottom:1rem; }
.empty-state h4 { color:var(--dark-300); font-size:1.1rem; margin-bottom:0.5rem; }
.empty-state p { color:var(--dark-500); font-size:0.85rem; }
@@media(max-width:768px) {
.header-row { flex-direction:column; }
.stats-row { grid-template-columns:1fr; }
.tbl-head { flex-direction:column; align-items:flex-start; }
.role-table { display:block; overflow-x:auto; }
.modal-box { max-width:100%; }
}
</style>
}
<div class="nex-bg"><div class="grid"></div><div class="mesh"></div></div>
<!-- Header -->
<section class="page-header">
<div class="container">
<div class="breadcrumb-nex">
<a href="@Url.Action("Index", "Dashboard")"><i class="fa-solid fa-gauge-high"></i> Admin</a>
<i class="fa-solid fa-chevron-right sep"></i>
<span class="current">Role Management</span>
</div>
<div class="header-row">
<div>
<div class="header-badge"><span class="dot"></span> PERMISSION & ACCESS CONTROL</div>
<h1 class="header-title">Role <span class="grad">Management</span></h1>
<p class="header-sub">Define roles and assign granular permissions for system access</p>
</div>
<div class="header-actions">
<button type="button" class="h-btn pri" onclick="if(checkPermission(rolePermissions.canCreate)) openCreateModal()">
<i class="fa-solid fa-plus"></i> New Role
</button>
<button type="button" class="h-btn danger d-none" id="bulkDeleteBtn"
onclick="if(checkPermission(rolePermissions.canDelete)) confirmBulkDelete()">
<i class="fa-solid fa-trash-can"></i> Delete Selected
</button>
</div>
</div>
</div>
</section>
<!-- Stats -->
<section style="position:relative;z-index:10;padding:0 0 1.5rem">
<div class="container">
@{
var totalRoles = Model.Count();
var totalPerms = Permissions.GetAll().Count;
var avgPerms = totalRoles > 0 ? (int)Model.Average(r => r.SelectedPermissions?.Count ?? 0) : 0;
}
<div class="stats-row">
<div class="stat-chip">
<div class="s-icon orange"><i class="fa-solid fa-shield-halved"></i></div>
<div><div class="s-val">@totalRoles</div><div class="s-lbl">Total Roles</div></div>
</div>
<div class="stat-chip">
<div class="s-icon purple"><i class="fa-solid fa-key"></i></div>
<div><div class="s-val">@totalPerms</div><div class="s-lbl">Available Permissions</div></div>
</div>
<div class="stat-chip">
<div class="s-icon blue"><i class="fa-solid fa-chart-bar"></i></div>
<div><div class="s-val">@avgPerms</div><div class="s-lbl">Avg Permissions / Role</div></div>
</div>
</div>
</div>
</section>
<!-- Roles Table -->
<section style="position:relative;z-index:10;padding-bottom:3rem;">
<div class="container">
<partial name="_Notification" />
<form asp-action="DeleteMultiple" method="post" id="deleteForm">
@Html.AntiForgeryToken()
<div class="nex-card">
<div class="top-glow"></div>
<div class="tbl-head">
<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap;">
<div class="tbl-title"><i class="fa-solid fa-shield-halved"></i> System Roles</div>
<div class="tbl-count">@totalRoles defined</div>
</div>
</div>
@if (Model.Any())
{
<table class="role-table" id="roleTable">
<thead>
<tr>
<th><input type="checkbox" class="nex-check" id="selectAll" onclick="toggleAll(this.checked)" /></th>
<th>Role</th>
<th>Permissions</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@{
var roleColors = new Dictionary<string, string> {
{"admin", "#f87171"}, {"user", "#60a5fa"}, {"manager", "#fbbf24"},
{"developer", "#c084fc"}, {"viewer", "#34d399"}, {"editor", "#22d3ee"}
};
var roleIcons = new Dictionary<string, string> {
{"admin", "fa-solid fa-crown"}, {"user", "fa-solid fa-user"},
{"manager", "fa-solid fa-user-tie"}, {"developer", "fa-solid fa-code"},
{"viewer", "fa-solid fa-eye"}, {"editor", "fa-solid fa-pen-nib"}
};
}
@foreach (var role in Model)
{
var key = role.Name?.ToLower() ?? "";
var color = roleColors.ContainsKey(key) ? roleColors[key] : "#fb923c";
var icon = roleIcons.ContainsKey(key) ? roleIcons[key] : "fa-solid fa-shield-halved";
var permCount = role.SelectedPermissions?.Count ?? 0;
var permsStr = role.SelectedPermissions != null ? string.Join(",", role.SelectedPermissions) : "";
<tr>
<td><input type="checkbox" class="nex-check" name="selectedRoles" value="@role.Id" onchange="updateSelectionCount()" /></td>
<td>
<div class="role-icon" style="background:@(color)20; color:@color"><i class="@icon"></i></div>
<span class="role-name">@role.Name</span>
</td>
<td>
@if (permCount == 0)
{
<span style="font-size:0.78rem;color:var(--dark-500);font-style:italic"><i class="fa-solid fa-lock"></i> No permissions assigned</span>
}
else if (permCount == totalPerms)
{
<span class="perm-pill has"><i class="fa-solid fa-check"></i> All permissions (@permCount)</span>
}
else
{
@foreach (var perm in role.SelectedPermissions!.Take(4))
{
<span class="perm-pill has"><i class="fa-solid fa-check"></i> @perm.Split('.').Last()</span>
}
@if (permCount > 4)
{
<span class="perm-more" onclick="if(checkPermission(rolePermissions.canEdit)) openEditModal('@role.Id')">+@(permCount - 4) more</span>
}
}
</td>
<td>
<button type="button" class="act-btn edit" title="Edit role"
onclick="if(checkPermission(rolePermissions.canEdit)) openEditModal('@role.Id')">
<i class="fa-solid fa-pen-to-square"></i>
</button>
<button type="button" class="act-btn del" title="Delete role"
onclick="if(checkPermission(rolePermissions.canDelete)) openDeleteModal('@role.Id', '@Html.Raw(role.Name?.Replace("'", "\\'"))')">
<i class="fa-solid fa-trash-can"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
}
else
{
<div class="empty-state">
<i class="fa-solid fa-shield-halved"></i>
<h4>No roles defined</h4>
<p>Create your first role to start managing access</p>
</div>
}
<div class="tbl-foot">
<div class="selected-info"><span id="selCount">0</span> of @totalRoles selected</div>
<div style="font-family:var(--font-mono);font-size:0.7rem;color:var(--dark-500);">@DateTime.Now.ToString("MMM dd, yyyy HH:mm")</div>
</div>
</div>
</form>
</div>
</section>
<!-- ═══════════ CREATE MODAL ═══════════ -->
<div class="modal-overlay" id="createModal">
<div class="modal-box">
<div class="m-glow orange"></div>
<div class="m-header">
<div class="m-header-left">
<div class="m-header-icon orange"><i class="fa-solid fa-plus"></i></div>
<div><h3>Create New Role</h3><p>Define a role with specific permissions</p></div>
</div>
<button type="button" class="m-close" onclick="closeModal('createModal')"><i class="fa-solid fa-xmark"></i></button>
</div>
<form id="createForm" novalidate>
@Html.AntiForgeryToken()
<div class="m-body">
<div id="createErrors" class="modal-errors"><ul></ul></div>
<div class="f-group">
<label class="f-label"><i class="fa-solid fa-shield-halved"></i> Role Name *</label>
<input type="text" name="Name" class="f-input" placeholder="e.g. Developer, Viewer, Manager" required />
</div>
<div class="f-group">
<label class="f-label"><i class="fa-solid fa-key"></i> Permissions</label>
<div class="perm-groups" id="createPermGroups">
@if (permissionGroups != null)
{
@foreach (var group in permissionGroups)
{
<div class="perm-group">
<div class="perm-group-head">
<h5><i class="fa-solid fa-folder"></i> @group.Key</h5>
<button type="button" class="perm-group-toggle" onclick="toggleGroupPerms(this, 'create')">Select All</button>
</div>
<div class="perm-group-body">
@foreach (var perm in group.Value)
{
<div class="perm-chip">
<input type="checkbox" name="SelectedPermissions" value="@perm.Value" id="create_@perm.Value.Replace(".", "_")" />
<label for="create_@perm.Value.Replace(".", "_")"><i class="@perm.Icon"></i> @perm.DisplayName</label>
</div>
}
</div>
</div>
}
}
</div>
</div>
</div>
<div class="m-footer">
<button type="button" class="h-btn sec" onclick="closeModal('createModal')"><i class="fa-solid fa-xmark"></i> Cancel</button>
<button type="submit" class="h-btn pri" id="createSubmitBtn">
<div class="btn-spinner" id="createSpinner"></div>
<i class="fa-solid fa-plus" id="createIcon"></i> Create Role
</button>
</div>
</form>
</div>
</div>
<!-- ═══════════ EDIT MODAL ═══════════ -->
<div class="modal-overlay" id="editModal">
<div class="modal-box">
<div class="m-glow blue"></div>
<div class="m-header">
<div class="m-header-left">
<div class="m-header-icon blue"><i class="fa-solid fa-pen-to-square"></i></div>
<div><h3>Edit Role</h3><p>Update role name and permissions</p></div>
</div>
<button type="button" class="m-close" onclick="closeModal('editModal')"><i class="fa-solid fa-xmark"></i></button>
</div>
<form id="editForm" novalidate>
@Html.AntiForgeryToken()
<input type="hidden" name="Id" id="editId" />
<div class="m-body">
<div id="editErrors" class="modal-errors"><ul></ul></div>
<div class="f-group">
<label class="f-label"><i class="fa-solid fa-shield-halved"></i> Role Name *</label>
<input type="text" name="Name" id="editName" class="f-input" required />
</div>
<div class="f-group">
<label class="f-label"><i class="fa-solid fa-key"></i> Permissions</label>
<div class="perm-groups" id="editPermGroups">
@if (permissionGroups != null)
{
@foreach (var group in permissionGroups)
{
<div class="perm-group">
<div class="perm-group-head">
<h5><i class="fa-solid fa-folder"></i> @group.Key</h5>
<button type="button" class="perm-group-toggle" onclick="toggleGroupPerms(this, 'edit')">Select All</button>
</div>
<div class="perm-group-body">
@foreach (var perm in group.Value)
{
<div class="perm-chip">
<input type="checkbox" name="SelectedPermissions" value="@perm.Value" id="edit_@perm.Value.Replace(".", "_")" />
<label for="edit_@perm.Value.Replace(".", "_")"><i class="@perm.Icon"></i> @perm.DisplayName</label>
</div>
}
</div>
</div>
}
}
</div>
</div>
</div>
<div class="m-footer">
<button type="button" class="h-btn sec" onclick="closeModal('editModal')"><i class="fa-solid fa-xmark"></i> Cancel</button>
<button type="submit" class="h-btn pri" id="editSubmitBtn" style="background:linear-gradient(135deg,var(--nex-blue),var(--nex-cyan))">
<div class="btn-spinner" id="editSpinner"></div>
<i class="fa-solid fa-floppy-disk" id="editIcon"></i> Save Changes
</button>
</div>
</form>
</div>
</div>
<!-- ═══════════ DELETE MODAL ═══════════ -->
<div class="modal-overlay" id="deleteModal">
<div class="modal-box" style="max-width:460px">
<div class="m-glow red"></div>
<div class="m-header">
<div class="m-header-left">
<div class="m-header-icon red"><i class="fa-solid fa-triangle-exclamation"></i></div>
<div><h3>Delete Role</h3><p>This action cannot be undone</p></div>
</div>
<button type="button" class="m-close" onclick="closeModal('deleteModal')"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="m-body">
<p class="del-warning">
<i class="fa-solid fa-circle-exclamation"></i> You are about to <strong>permanently delete</strong> the role
"<strong id="delRoleName">Role</strong>". All users currently assigned this role will lose its associated permissions.
This action is <strong>irreversible</strong>.
</p>
</div>
<div class="m-footer">
<button type="button" class="h-btn sec" onclick="closeModal('deleteModal')"><i class="fa-solid fa-xmark"></i> Cancel</button>
<form id="singleDeleteForm" asp-action="DeleteMultiple" method="post" style="display:inline">
@Html.AntiForgeryToken()
<input type="hidden" name="selectedRoles" id="delRoleId" />
<button type="submit" class="h-btn danger"><i class="fa-solid fa-trash-can"></i> Delete Role</button>
</form>
</div>
</div>
</div>
<!-- ═══════════ BULK DELETE MODAL ═══════════ -->
<div class="modal-overlay" id="bulkDeleteModal">
<div class="modal-box" style="max-width:460px">
<div class="m-glow red"></div>
<div class="m-header">
<div class="m-header-left">
<div class="m-header-icon red"><i class="fa-solid fa-shield-halved"></i></div>
<div><h3>Delete Selected Roles</h3><p>Bulk removal confirmation</p></div>
</div>
<button type="button" class="m-close" onclick="closeModal('bulkDeleteModal')"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="m-body">
<p class="del-warning">
<i class="fa-solid fa-circle-exclamation"></i> You are about to <strong>permanently delete <span id="bulkCount">0</span> roles</strong>.
All users assigned these roles will lose their associated permissions. This is <strong>irreversible</strong>.
</p>
</div>
<div class="m-footer">
<button type="button" class="h-btn sec" onclick="closeModal('bulkDeleteModal')"><i class="fa-solid fa-xmark"></i> Cancel</button>
<button type="button" class="h-btn danger" onclick="submitBulkDelete()"><i class="fa-solid fa-trash-can"></i> Delete All</button>
</div>
</div>
</div>
@section Scripts {
<script>
const rolePermissions = {
canCreate: @User.HasPermission(Permissions.Roles.Create).ToString().ToLower(),
canEdit: @User.HasPermission(Permissions.Roles.Edit).ToString().ToLower(),
canDelete: @User.HasPermission(Permissions.Roles.Delete).ToString().ToLower()
};
const accessDeniedUrl = '@Url.Action("Index", "AccessDenied", new { area = "Admin" })';
function checkPermission(permission) {
if (!permission) {
window.location.href = accessDeniedUrl;
return false;
}
return true;
}
</script>
<script>
function showToast(message, type) {
const toast = document.createElement('div');
toast.className = 'nex-toast ' + type;
const icon = type === 'success' ? 'fa-circle-check' : 'fa-circle-xmark';
toast.innerHTML = '<i class="fa-solid ' + icon + '"></i> ' + message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3200);
}
// Selection
function toggleAll(checked) {
document.querySelectorAll('input[name="selectedRoles"]').forEach(cb => cb.checked = checked);
updateSelectionCount();
}
function updateSelectionCount() {
const checked = document.querySelectorAll('input[name="selectedRoles"]:checked').length;
document.getElementById('selCount').textContent = checked;
const bulkBtn = document.getElementById('bulkDeleteBtn');
if (checked > 0) bulkBtn.classList.remove('d-none');
else bulkBtn.classList.add('d-none');
}
// Modal helpers
function openModal(id) { document.getElementById(id).classList.add('active'); document.body.style.overflow = 'hidden'; }
function closeModal(id) { document.getElementById(id).classList.remove('active'); document.body.style.overflow = ''; }
document.querySelectorAll('.modal-overlay').forEach(o => {
o.addEventListener('click', e => { if (e.target === o) closeModal(o.id); });
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape') document.querySelectorAll('.modal-overlay.active').forEach(m => closeModal(m.id));
});
function showModalErrors(containerId, errors) {
const c = document.getElementById(containerId);
const ul = c.querySelector('ul');
ul.innerHTML = '';
errors.forEach(err => {
const li = document.createElement('li');
li.innerHTML = '<i class="fa-solid fa-circle-xmark"></i> ' + err;
ul.appendChild(li);
});
c.style.display = 'block';
}
function hideModalErrors(id) { document.getElementById(id).style.display = 'none'; }
function setLoading(btnId, spinnerId, iconId, loading) {
const btn = document.getElementById(btnId);
const spinner = document.getElementById(spinnerId);
const icon = document.getElementById(iconId);
btn.disabled = loading;
spinner.style.display = loading ? 'block' : 'none';
if (icon) icon.style.display = loading ? 'none' : '';
}
// Toggle group permissions
function toggleGroupPerms(btn, prefix) {
const body = btn.closest('.perm-group').querySelector('.perm-group-body');
const checkboxes = body.querySelectorAll('input[type="checkbox"]');
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
checkboxes.forEach(cb => cb.checked = !allChecked);
btn.textContent = allChecked ? 'Select All' : 'Deselect All';
}
// ── CREATE ──
function openCreateModal() {
document.getElementById('createForm').reset();
hideModalErrors('createErrors');
openModal('createModal');
}
document.getElementById('createForm').addEventListener('submit', function(e) {
e.preventDefault();
hideModalErrors('createErrors');
const name = this.querySelector('[name="Name"]').value.trim();
if (!name) { showModalErrors('createErrors', ['Role name is required.']); return; }
setLoading('createSubmitBtn', 'createSpinner', 'createIcon', true);
fetch('@Url.Action("CreateAjax")', { method: 'POST', body: new FormData(this) })
.then(res => res.json())
.then(data => {
setLoading('createSubmitBtn', 'createSpinner', 'createIcon', false);
if (data.success) {
closeModal('createModal');
showToast(data.message, 'success');
setTimeout(() => location.reload(), 800);
} else {
showModalErrors('createErrors', data.errors || ['An error occurred.']);
}
})
.catch(() => {
setLoading('createSubmitBtn', 'createSpinner', 'createIcon', false);
showModalErrors('createErrors', ['Network error. Please try again.']);
});
});
// ── EDIT ──
function openEditModal(roleId) {
hideModalErrors('editErrors');
document.querySelectorAll('#editPermGroups input[type="checkbox"]').forEach(cb => cb.checked = false);
// Fetch role data with permissions
fetch('@Url.Action("GetRolePermissions")' + '?id=' + roleId)
.then(res => res.json())
.then(data => {
if (data.success) {
document.getElementById('editId').value = data.id;
document.getElementById('editName').value = data.name;
// Check matching permissions
if (data.permissions) {
data.permissions.forEach(p => {
const cb = document.getElementById('edit_' + p.replace(/\./g, '_'));
if (cb) cb.checked = true;
});
}
// Update group toggle buttons
document.querySelectorAll('#editPermGroups .perm-group').forEach(group => {
const cbs = group.querySelectorAll('.perm-group-body input[type="checkbox"]');
const btn = group.querySelector('.perm-group-toggle');
const allChecked = Array.from(cbs).every(cb => cb.checked);
btn.textContent = allChecked ? 'Deselect All' : 'Select All';
});
openModal('editModal');
} else {
showToast('Failed to load role data.', 'error');
}
})
.catch(() => showToast('Network error.', 'error'));
}
document.getElementById('editForm').addEventListener('submit', function(e) {
e.preventDefault();
hideModalErrors('editErrors');
const name = this.querySelector('[name="Name"]').value.trim();
if (!name) { showModalErrors('editErrors', ['Role name is required.']); return; }
setLoading('editSubmitBtn', 'editSpinner', 'editIcon', true);
fetch('@Url.Action("EditAjax")', { method: 'POST', body: new FormData(this) })
.then(res => res.json())
.then(data => {
setLoading('editSubmitBtn', 'editSpinner', 'editIcon', false);
if (data.success) {
closeModal('editModal');
showToast(data.message, 'success');
setTimeout(() => location.reload(), 800);
} else {
showModalErrors('editErrors', data.errors || ['An error occurred.']);
}
})
.catch(() => {
setLoading('editSubmitBtn', 'editSpinner', 'editIcon', false);
showModalErrors('editErrors', ['Network error. Please try again.']);
});
});
// ── DELETE ──
function openDeleteModal(roleId, roleName) {
document.getElementById('delRoleId').value = roleId;
document.getElementById('delRoleName').textContent = roleName;
openModal('deleteModal');
}
function confirmBulkDelete() {
const count = document.querySelectorAll('input[name="selectedRoles"]:checked').length;
if (count === 0) return;
document.getElementById('bulkCount').textContent = count;
openModal('bulkDeleteModal');
}
function submitBulkDelete() {
closeModal('bulkDeleteModal');
document.getElementById('deleteForm').submit();
}
</script>
}