Initial commit
This commit is contained in:
@@ -0,0 +1,440 @@
|
||||
@model PagedResult<PowderCoating.Application.DTOs.User.PlatformUserListDto>
|
||||
|
||||
@section Styles {
|
||||
<style>
|
||||
.avatar-circle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
[data-bs-theme="dark"] a.text-dark { color: var(--bs-body-color) !important; }
|
||||
</style>
|
||||
}
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Platform Users";
|
||||
ViewData["PageIcon"] = "bi-people-fill";
|
||||
var currentFilter = ViewBag.CurrentFilter as string;
|
||||
var returnUrl = Url.Action("Index", "PlatformUsers");
|
||||
const string RootUserEmail = "artemis@powdercoatinglogix.com";
|
||||
}
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<a asp-action="GrantSuperAdmin" class="btn btn-outline-danger">
|
||||
<i class="bi bi-shield-plus"></i> Grant SuperAdmin Access
|
||||
</a>
|
||||
<a asp-action="CreateSuperAdmin" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Create SuperAdmin
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-7">
|
||||
<div class="btn-group" role="group">
|
||||
<a asp-action="Index" class="btn btn-outline-primary @(string.IsNullOrEmpty(currentFilter) ? "active" : "")">
|
||||
<i class="bi bi-people"></i> All Users
|
||||
</a>
|
||||
<a asp-action="Index" asp-route-filter="superadmins" class="btn btn-outline-danger @(currentFilter == "superadmins" ? "active" : "")">
|
||||
<i class="bi bi-shield-check"></i> SuperAdmins
|
||||
</a>
|
||||
<a asp-action="Index" asp-route-filter="companyadmins" class="btn btn-outline-primary @(currentFilter == "companyadmins" ? "active" : "")">
|
||||
<i class="bi bi-building"></i> Company Admins
|
||||
</a>
|
||||
<a asp-action="Index" asp-route-filter="active" class="btn btn-outline-success @(currentFilter == "active" ? "active" : "")">
|
||||
<i class="bi bi-check-circle"></i> Active
|
||||
</a>
|
||||
<a asp-action="Index" asp-route-filter="inactive" class="btn btn-outline-secondary @(currentFilter == "inactive" ? "active" : "")">
|
||||
<i class="bi bi-x-circle"></i> Inactive
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<form method="get" class="d-flex gap-2">
|
||||
<input type="hidden" name="filter" value="@currentFilter" />
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-white border-end-0">
|
||||
<i class="bi bi-search text-muted"></i>
|
||||
</span>
|
||||
<input type="text" name="searchTerm" class="form-control border-start-0"
|
||||
placeholder="Search by name or email..." value="@ViewBag.SearchTerm">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-search"></i></button>
|
||||
@if (!string.IsNullOrEmpty(ViewBag.SearchTerm))
|
||||
{
|
||||
<a asp-action="Index" asp-route-filter="@currentFilter" class="btn btn-outline-secondary">Clear</a>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
@if (Model.Items.Any())
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th sortable="Name" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Name</th>
|
||||
<th sortable="Email" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Email</th>
|
||||
<th>Role</th>
|
||||
<th>Company</th>
|
||||
<th sortable="IsActive" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Status</th>
|
||||
<th sortable="LastLoginDate" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Last Login</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var user in Model.Items)
|
||||
{
|
||||
<tr style="cursor:pointer" onclick="window.location='@Url.Action("Details", new { id = user.Id })'">
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar-circle bg-primary text-white me-2">
|
||||
@user.FirstName[0]@user.LastName[0]
|
||||
</div>
|
||||
<div>
|
||||
<strong>@user.FullName</strong>
|
||||
@if (!string.IsNullOrEmpty(user.Position))
|
||||
{
|
||||
<br /><small class="text-muted">@user.Position</small>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@user.Email
|
||||
@if (!string.IsNullOrEmpty(user.EmployeeNumber))
|
||||
{
|
||||
<br /><small class="text-muted">ID: @user.EmployeeNumber</small>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (user.IsSuperAdmin)
|
||||
{
|
||||
<span class="badge bg-danger">
|
||||
<i class="bi bi-shield-check"></i> SuperAdmin
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-primary">
|
||||
<i class="bi bi-person-gear"></i> Company Admin
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (!string.IsNullOrEmpty(user.CompanyName))
|
||||
{
|
||||
<small>@user.CompanyName</small>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">-</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (user.IsBanned)
|
||||
{
|
||||
<span class="badge bg-danger">
|
||||
<i class="bi bi-slash-circle"></i> Banned
|
||||
</span>
|
||||
}
|
||||
else if (user.IsActive)
|
||||
{
|
||||
<span class="badge bg-success">
|
||||
<i class="bi bi-check-circle"></i> Active
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary">
|
||||
<i class="bi bi-x-circle"></i> Inactive
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (user.LastLoginDate.HasValue)
|
||||
{
|
||||
@user.LastLoginDate.Value.ToString("MMM dd, yyyy")
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Never</span>
|
||||
}
|
||||
</td>
|
||||
<td onclick="event.stopPropagation()">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a asp-action="Details" asp-route-id="@user.Id" class="btn btn-outline-info" title="View Details">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
@if (string.Equals(user.Email, RootUserEmail, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@if (string.Equals(User.Identity?.Name, RootUserEmail, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
<a asp-action="Edit" asp-route-id="@user.Id" class="btn btn-outline-primary" title="Edit">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="btn btn-outline-secondary disabled" title="Root account — protected">
|
||||
<i class="bi bi-shield-lock"></i>
|
||||
</span>
|
||||
}
|
||||
}
|
||||
else if (user.IsSuperAdmin)
|
||||
{
|
||||
<a asp-action="Edit" asp-route-id="@user.Id" class="btn btn-outline-primary" title="Edit SuperAdmin">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
title="Revoke SuperAdmin Access"
|
||||
onclick="openRevokeModal('@user.Id', '@Html.Encode(user.FullName)')">
|
||||
<i class="bi bi-shield-x"></i>
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a asp-controller="CompanyUsers" asp-action="Edit" asp-route-id="@user.Id" asp-route-returnUrl="@returnUrl" class="btn btn-outline-primary" title="Edit User">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-warning"
|
||||
title="Reset Password (set manually)"
|
||||
onclick="openResetPasswordModal('@user.Id', '@Html.Encode(user.FullName)')">
|
||||
<i class="bi bi-key"></i>
|
||||
</button>
|
||||
<form asp-controller="CompanyUsers"
|
||||
asp-action="SendPasswordResetEmail"
|
||||
asp-route-id="@user.Id"
|
||||
method="post"
|
||||
class="d-inline"
|
||||
onsubmit="return confirm('Send a password reset link to @Html.Encode(user.Email)?')">
|
||||
@Html.AntiForgeryToken()
|
||||
<button type="submit" class="btn btn-outline-info" title="Email password reset link">
|
||||
<i class="bi bi-envelope-arrow-up"></i>
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Mobile card view — shown on screens < 992px (table-responsive hidden by mobile-cards.css) -->
|
||||
<div class="mobile-card-view">
|
||||
<div class="mobile-card-list">
|
||||
@foreach (var user in Model.Items)
|
||||
{
|
||||
<a href="@Url.Action("Details", new { id = user.Id })" class="mobile-data-card text-decoration-none">
|
||||
<div class="mobile-card-header">
|
||||
<div class="mobile-card-icon bg-primary"><i class="bi bi-person-fill"></i></div>
|
||||
<div class="mobile-card-title">
|
||||
<h6>@user.FullName</h6>
|
||||
<small class="text-muted">@user.Email</small>
|
||||
</div>
|
||||
@if (user.IsBanned)
|
||||
{
|
||||
<span class="badge bg-danger"><i class="bi bi-slash-circle"></i> Banned</span>
|
||||
}
|
||||
else if (user.IsActive)
|
||||
{
|
||||
<span class="badge bg-success">Active</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary">Inactive</span>
|
||||
}
|
||||
</div>
|
||||
<div class="mobile-card-body">
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Role</span>
|
||||
<span class="mobile-card-value">
|
||||
@if (user.IsSuperAdmin)
|
||||
{
|
||||
<span class="badge bg-danger"><i class="bi bi-shield-check"></i> SuperAdmin</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-primary"><i class="bi bi-person-gear"></i> Company Admin</span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(user.CompanyName))
|
||||
{
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Company</span>
|
||||
<span class="mobile-card-value">@user.CompanyName</span>
|
||||
</div>
|
||||
}
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Last Login</span>
|
||||
<span class="mobile-card-value">
|
||||
@if (user.LastLoginDate.HasValue)
|
||||
{
|
||||
@user.LastLoginDate.Value.ToString("MMM dd, yyyy")
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Never</span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-card-footer">
|
||||
<span class="btn btn-sm btn-outline-primary">View →</span>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-inbox display-1 text-muted"></i>
|
||||
<p class="text-muted mt-3">No users found</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (Model.TotalCount > 0)
|
||||
{
|
||||
@await Html.PartialAsync("_Pagination", Model)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reset Password Modal -->
|
||||
<div class="modal fade" id="resetPasswordModal" tabindex="-1" aria-labelledby="resetPasswordModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="resetPasswordModalLabel">
|
||||
<i class="bi bi-key"></i> Reset Password
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form asp-action="ResetPassword" method="post" id="resetPasswordForm">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="id" id="resetUserId" />
|
||||
<input type="hidden" name="returnUrl" value="@returnUrl" />
|
||||
<div class="modal-body">
|
||||
<p class="mb-3">
|
||||
Resetting password for: <strong id="resetUserName"></strong>
|
||||
</p>
|
||||
<div class="mb-3">
|
||||
<label for="newPassword" class="form-label">New Password *</label>
|
||||
<input type="password" class="form-control" id="newPassword" name="newPassword"
|
||||
placeholder="Minimum 8 characters" required minlength="8" />
|
||||
<div class="invalid-feedback">Password must be at least 8 characters.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="confirmNewPassword" class="form-label">Confirm New Password *</label>
|
||||
<input type="password" class="form-control" id="confirmNewPassword"
|
||||
placeholder="Re-enter new password" required />
|
||||
<div class="invalid-feedback" id="confirmPasswordError">Passwords do not match.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-warning">
|
||||
<i class="bi bi-key"></i> Reset Password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Revoke SuperAdmin Modal -->
|
||||
<div class="modal fade" id="revokeModal" tabindex="-1" aria-labelledby="revokeModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger text-white">
|
||||
<h5 class="modal-title" id="revokeModalLabel">
|
||||
<i class="bi bi-shield-x"></i> Revoke SuperAdmin Access
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form asp-action="RevokeSuperAdmin" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="id" id="revokeUserId" />
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-warning">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
<strong>Warning:</strong> This will remove platform-wide access. The user will be demoted to Company Admin.
|
||||
</div>
|
||||
<p class="mb-0">
|
||||
Are you sure you want to revoke SuperAdmin access from <strong id="revokeUserName"></strong>?
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-shield-x"></i> Revoke Access
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function openResetPasswordModal(userId, userName) {
|
||||
document.getElementById('resetUserId').value = userId;
|
||||
document.getElementById('resetUserName').textContent = userName;
|
||||
document.getElementById('newPassword').value = '';
|
||||
document.getElementById('confirmNewPassword').value = '';
|
||||
document.getElementById('newPassword').classList.remove('is-invalid');
|
||||
document.getElementById('confirmNewPassword').classList.remove('is-invalid');
|
||||
var modal = new bootstrap.Modal(document.getElementById('resetPasswordModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function openRevokeModal(userId, userName) {
|
||||
document.getElementById('revokeUserId').value = userId;
|
||||
document.getElementById('revokeUserName').textContent = userName;
|
||||
var modal = new bootstrap.Modal(document.getElementById('revokeModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
document.getElementById('resetPasswordForm').addEventListener('submit', function (e) {
|
||||
var pwd = document.getElementById('newPassword');
|
||||
var confirm = document.getElementById('confirmNewPassword');
|
||||
var valid = true;
|
||||
|
||||
if (pwd.value.length < 8) {
|
||||
pwd.classList.add('is-invalid');
|
||||
valid = false;
|
||||
} else {
|
||||
pwd.classList.remove('is-invalid');
|
||||
}
|
||||
|
||||
if (pwd.value !== confirm.value) {
|
||||
confirm.classList.add('is-invalid');
|
||||
document.getElementById('confirmPasswordError').textContent = 'Passwords do not match.';
|
||||
valid = false;
|
||||
} else {
|
||||
confirm.classList.remove('is-invalid');
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
Reference in New Issue
Block a user