Files
PowderCoatingLogix/src/PowderCoating.Web/Views/PlatformUsers/Index.cshtml
T
2026-04-23 21:38:24 -04:00

441 lines
24 KiB
Plaintext

@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>
}