Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -0,0 +1,115 @@
@model PowderCoating.Application.DTOs.User.CreateSuperAdminDto
@{
ViewData["Title"] = "Create SuperAdmin";
}
<div class="container-fluid py-4">
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">
<i class="bi bi-shield-check"></i> Create New SuperAdmin
</h4>
</div>
<div class="card-body">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
<strong>Important:</strong> SuperAdmins have full platform access across all companies. Use this feature carefully.
</div>
<form asp-action="CreateSuperAdmin" method="post">
<partial name="_ValidationSummary" />
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="FirstName" class="form-label">First Name <span class="text-danger">*</span></label>
<input asp-for="FirstName" class="form-control" placeholder="Enter first name" required />
<span asp-validation-for="FirstName" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="LastName" class="form-label">Last Name <span class="text-danger">*</span></label>
<input asp-for="LastName" class="form-control" placeholder="Enter last name" required />
<span asp-validation-for="LastName" class="text-danger"></span>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="Email" class="form-label">Email <span class="text-danger">*</span></label>
<input asp-for="Email" type="email" class="form-control" placeholder="admin@example.com" required />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="EmployeeNumber" class="form-label">Employee Number</label>
<input asp-for="EmployeeNumber" class="form-control" placeholder="SA-002" />
<span asp-validation-for="EmployeeNumber" class="text-danger"></span>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="Department" class="form-label">Department</label>
<input asp-for="Department" class="form-control" placeholder="Platform" />
<span asp-validation-for="Department" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Position" class="form-label">Position</label>
<input asp-for="Position" class="form-control" placeholder="Super Administrator" />
<span asp-validation-for="Position" class="text-danger"></span>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="Phone" class="form-label">Phone</label>
<input asp-for="Phone" type="tel" class="form-control" placeholder="(555) 123-4567" />
<span asp-validation-for="Phone" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="HireDate" class="form-label">Hire Date <span class="text-danger">*</span></label>
<input asp-for="HireDate" type="date" class="form-control" required />
<span asp-validation-for="HireDate" class="text-danger"></span>
</div>
</div>
<hr class="my-4" />
<h5 class="mb-3">
<i class="bi bi-key"></i> Password Setup
</h5>
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="Password" class="form-label">Password <span class="text-danger">*</span></label>
<input asp-for="Password" type="password" class="form-control" placeholder="Enter strong password" required />
<span asp-validation-for="Password" class="text-danger"></span>
<small class="form-text text-muted">
Minimum 8 characters, must include uppercase, lowercase, number, and special character
</small>
</div>
<div class="col-md-6">
<label asp-for="ConfirmPassword" class="form-label">Confirm Password <span class="text-danger">*</span></label>
<input asp-for="ConfirmPassword" type="password" class="form-control" placeholder="Re-enter password" required />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
</div>
<div class="d-flex justify-content-between mt-4">
<a asp-action="Index" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Cancel
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-shield-check"></i> Create SuperAdmin
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
@@ -0,0 +1,379 @@
@model PowderCoating.Application.DTOs.User.SuperAdminDetailsDto
@{
ViewData["Title"] = "User Details";
var isSuperAdmin = ViewBag.IsSuperAdmin as bool? ?? false;
var companyRole = ViewBag.CompanyRole as string;
var companyName = ViewBag.CompanyName as string;
var isCompanyUser = !isSuperAdmin && !string.IsNullOrEmpty(companyRole);
var detailsReturnUrl = Url.Action("Details", "PlatformUsers", new { id = Model.Id });
}
<div class="container-fluid py-4">
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h4 class="mb-0">
<i class="bi bi-person-circle"></i> User Details
</h4>
<div class="d-flex gap-1">
@if (isSuperAdmin)
{
<span class="badge bg-danger">
<i class="bi bi-shield-check"></i> SuperAdmin
</span>
}
else if (companyRole == "CompanyAdmin")
{
<span class="badge bg-warning text-dark">
<i class="bi bi-person-gear"></i> Company Admin
</span>
}
else if (!string.IsNullOrEmpty(companyRole))
{
<span class="badge bg-light text-dark">
<i class="bi bi-person-badge"></i> @companyRole
</span>
}
@if (Model.IsBanned)
{
<span class="badge bg-danger"><i class="bi bi-slash-circle"></i> Banned</span>
}
else if (Model.IsActive)
{
<span class="badge bg-success">Active</span>
}
else
{
<span class="badge bg-secondary">Inactive</span>
}
</div>
</div>
<div class="card-body">
<div class="row mb-4">
<div class="col-md-12 text-center">
<div class="avatar-circle-large mx-auto mb-3">
@Model.FirstName[0]@Model.LastName[0]
</div>
<h3>@Model.FullName</h3>
@if (!string.IsNullOrEmpty(Model.Position))
{
<p class="text-muted mb-0">@Model.Position</p>
}
@if (!string.IsNullOrEmpty(Model.Department))
{
<p class="text-muted">@Model.Department</p>
}
@if (!string.IsNullOrEmpty(companyName))
{
<p class="text-muted mb-0"><i class="bi bi-building"></i> @companyName</p>
}
</div>
</div>
<hr />
<div class="row mb-3">
<div class="col-md-6">
<h6 class="text-muted mb-2">Contact Information</h6>
<p class="mb-1">
<strong><i class="bi bi-envelope"></i> Email:</strong><br />
@Model.Email
@if (Model.EmailConfirmed)
{
<i class="bi bi-check-circle-fill text-success" title="Email Confirmed"></i>
}
</p>
@if (!string.IsNullOrEmpty(Model.Phone))
{
<p class="mb-1">
<strong><i class="bi bi-telephone"></i> Phone:</strong><br />
@Model.Phone
</p>
}
@if (!string.IsNullOrEmpty(Model.EmployeeNumber))
{
<p class="mb-1">
<strong><i class="bi bi-badge-4k"></i> Employee Number:</strong><br />
@Model.EmployeeNumber
</p>
}
</div>
<div class="col-md-6">
<h6 class="text-muted mb-2">Employment Information</h6>
<p class="mb-1">
<strong><i class="bi bi-calendar-check"></i> Hire Date:</strong><br />
@Model.HireDate.ToString("MMMM dd, yyyy")
</p>
@if (Model.TerminationDate.HasValue)
{
<p class="mb-1">
<strong><i class="bi bi-calendar-x"></i> Termination Date:</strong><br />
@Model.TerminationDate.Value.ToString("MMMM dd, yyyy")
</p>
}
@if (Model.LastLoginDate.HasValue)
{
<p class="mb-1">
<strong><i class="bi bi-clock-history"></i> Last Login:</strong><br />
@Model.LastLoginDate.Value.ToString("MMMM dd, yyyy HH:mm")
</p>
}
else
{
<p class="mb-1">
<strong><i class="bi bi-clock-history"></i> Last Login:</strong><br />
<span class="text-muted">Never</span>
</p>
}
</div>
</div>
@if (Model.IsBanned)
{
<hr />
<div class="alert alert-danger mb-0">
<i class="bi bi-slash-circle-fill me-2"></i>
<strong>This account is banned.</strong>
@if (Model.BannedAt.HasValue)
{
<span class="ms-1 text-muted small">Since @Model.BannedAt.Value.ToString("MMMM dd, yyyy HH:mm")</span>
}
@if (!string.IsNullOrEmpty(Model.BanReason))
{
<div class="mt-1">Reason: @Model.BanReason</div>
}
</div>
}
@if (isCompanyUser)
{
<hr />
<h6 class="text-muted mb-3">Permissions</h6>
<div class="row g-2 mb-3">
@{
var perms = new[]
{
("Can Manage Jobs", (bool)(ViewBag.CanManageJobs ?? false)),
("Can Manage Inventory", (bool)(ViewBag.CanManageInventory ?? false)),
("Can Manage Customers", (bool)(ViewBag.CanManageCustomers ?? false)),
("Can Create Quotes", (bool)(ViewBag.CanCreateQuotes ?? false)),
("Can Approve Quotes", (bool)(ViewBag.CanApproveQuotes ?? false)),
("Can Manage Calendar", (bool)(ViewBag.CanManageCalendar ?? false)),
("Can View Calendar", (bool)(ViewBag.CanViewCalendar ?? false)),
("Can Manage Products", (bool)(ViewBag.CanManageProducts ?? false)),
("Can View Products", (bool)(ViewBag.CanViewProducts ?? false)),
("Can Manage Equipment", (bool)(ViewBag.CanManageEquipment ?? false)),
("Can Manage Vendors", (bool)(ViewBag.CanManageVendors ?? false)),
("Can Manage Maintenance", (bool)(ViewBag.CanManageMaintenance ?? false)),
};
foreach (var (label, granted) in perms)
{
<div class="col-md-6">
<span class="@(granted ? "text-success" : "text-muted")">
<i class="bi @(granted ? "bi-check-circle-fill" : "bi-x-circle")"></i>
@label
</span>
</div>
}
}
</div>
}
<hr />
<div class="row mb-3">
<div class="col-md-6">
<p class="mb-1 text-muted small">
<strong>Account Created:</strong> @Model.CreatedAt.ToString("MMMM dd, yyyy HH:mm")
</p>
</div>
<div class="col-md-6">
<p class="mb-1 text-muted small">
<strong>Last Updated:</strong> @(Model.UpdatedAt?.ToString("MMMM dd, yyyy HH:mm") ?? "Never")
</p>
</div>
</div>
<hr />
<div class="d-flex justify-content-between mt-4">
<a asp-action="Index" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Back to List
</a>
<div class="d-flex gap-2">
@if (!Model.IsBanned)
{
<button type="button" class="btn btn-outline-danger"
data-bs-toggle="modal" data-bs-target="#banModal">
<i class="bi bi-slash-circle"></i> Ban User
</button>
}
else
{
<form asp-controller="PlatformUsers" asp-action="UnbanUser" asp-route-id="@Model.Id"
asp-route-returnUrl="@detailsReturnUrl" method="post" class="d-inline"
onsubmit="return confirm('Lift the ban on @Html.Encode(Model.FullName)?')">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-success">
<i class="bi bi-check-circle"></i> Unban User
</button>
</form>
}
@if (isSuperAdmin)
{
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-primary">
<i class="bi bi-pencil"></i> Edit SuperAdmin
</a>
}
else if (isCompanyUser)
{
<button type="button" class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#resetPasswordModal"
title="Set password manually">
<i class="bi bi-key"></i> Reset Password
</button>
<form asp-controller="CompanyUsers"
asp-action="SendPasswordResetEmail"
asp-route-id="@Model.Id"
method="post"
class="d-inline"
onsubmit="return confirm('Send a password reset link to @Html.Encode(Model.Email)?')">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-outline-info" title="Email password reset link">
<i class="bi bi-envelope-arrow-up me-1"></i>Send Reset Link
</button>
</form>
<a asp-controller="CompanyUsers" asp-action="Edit" asp-route-id="@Model.Id" asp-route-returnUrl="@detailsReturnUrl" class="btn btn-primary">
<i class="bi bi-pencil"></i> Edit User
</a>
}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@if (isCompanyUser)
{
<!-- 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" value="@Model.Id" />
<input type="hidden" name="returnUrl" value="@detailsReturnUrl" />
<div class="modal-body">
<p class="mb-3">
Resetting password for: <strong>@Model.FullName</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>
}
<!-- Ban User Modal -->
<div class="modal fade" id="banModal" tabindex="-1" aria-labelledby="banModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="banModalLabel"><i class="bi bi-slash-circle"></i> Ban User</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form asp-controller="PlatformUsers" asp-action="BanUser" asp-route-id="@Model.Id"
asp-route-returnUrl="@detailsReturnUrl" method="post">
@Html.AntiForgeryToken()
<div class="modal-body">
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
Banning <strong>@Model.FullName</strong> will immediately prevent them from logging in.
</div>
<div class="mb-3">
<label for="banReason" class="form-label">Reason <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="banReason" name="reason"
placeholder="e.g. Unauthorized access attempt" required maxlength="500" />
</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-danger"><i class="bi bi-slash-circle"></i> Ban User</button>
</div>
</form>
</div>
</div>
</div>
<style>
.avatar-circle-large {
width: 100px;
height: 100px;
background-color: #007bff;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 2.5rem;
}
</style>
@if (isCompanyUser)
{
@section Scripts {
<script>
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>
}
}
@@ -0,0 +1,106 @@
@model PowderCoating.Application.DTOs.User.UpdateSuperAdminDto
@{
ViewData["Title"] = "Edit SuperAdmin";
}
<div class="container-fluid py-4">
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">
<i class="bi bi-pencil"></i> Edit SuperAdmin
</h4>
</div>
<div class="card-body">
<form asp-action="Edit" method="post">
<input type="hidden" asp-for="Id" />
<partial name="_ValidationSummary" />
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="FirstName" class="form-label">First Name <span class="text-danger">*</span></label>
<input asp-for="FirstName" class="form-control" required />
<span asp-validation-for="FirstName" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="LastName" class="form-label">Last Name <span class="text-danger">*</span></label>
<input asp-for="LastName" class="form-control" required />
<span asp-validation-for="LastName" class="text-danger"></span>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="Email" class="form-label">Email <span class="text-danger">*</span></label>
<input asp-for="Email" type="email" class="form-control" readonly />
<small class="form-text text-muted">Email cannot be changed</small>
</div>
<div class="col-md-6">
<label asp-for="EmployeeNumber" class="form-label">Employee Number</label>
<input asp-for="EmployeeNumber" class="form-control" />
<span asp-validation-for="EmployeeNumber" class="text-danger"></span>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="Department" class="form-label">Department</label>
<input asp-for="Department" class="form-control" />
<span asp-validation-for="Department" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Position" class="form-label">Position</label>
<input asp-for="Position" class="form-control" />
<span asp-validation-for="Position" class="text-danger"></span>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="Phone" class="form-label">Phone</label>
<input asp-for="Phone" type="tel" class="form-control" />
<span asp-validation-for="Phone" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="HireDate" class="form-label">Hire Date <span class="text-danger">*</span></label>
<input asp-for="HireDate" type="date" class="form-control" required />
<span asp-validation-for="HireDate" class="text-danger"></span>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="TerminationDate" class="form-label">Termination Date</label>
<input asp-for="TerminationDate" type="date" class="form-control" />
<span asp-validation-for="TerminationDate" class="text-danger"></span>
<small class="form-text text-muted">Leave blank if currently employed</small>
</div>
<div class="col-md-6">
<div class="form-check mt-4 pt-2">
<input asp-for="IsActive" class="form-check-input" type="checkbox" />
<label asp-for="IsActive" class="form-check-label">
Account is Active
</label>
</div>
</div>
</div>
<div class="d-flex justify-content-between mt-4">
<a asp-action="Index" asp-route-filter="superadmins" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Cancel
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i> Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
@@ -0,0 +1,191 @@
@{
ViewData["Title"] = "Grant SuperAdmin Access";
ViewData["PageIcon"] = "bi-shield-plus";
}
<div class="container-fluid py-4">
<div class="d-flex justify-content-end mb-4">
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back to Users
</a>
</div>
<div class="alert alert-danger">
<i class="bi bi-shield-exclamation me-2"></i>
<strong>Caution:</strong> SuperAdmins have full platform access across <em>all companies</em>. Only promote users you trust completely with platform-level administration.
</div>
<div class="card shadow-sm mb-4">
<div class="card-header bg-transparent">
<h5 class="mb-0"><i class="bi bi-search me-2"></i>Search for a User</h5>
</div>
<div class="card-body">
<div class="row g-2 align-items-end">
<div class="col-md-8">
<label for="searchInput" class="form-label">Name or Email</label>
<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" id="searchInput" class="form-control border-start-0"
placeholder="Start typing a name or email address..." />
</div>
</div>
<div class="col-md-auto">
<button type="button" class="btn btn-primary" onclick="searchUsers()">
<i class="bi bi-search"></i> Search
</button>
</div>
</div>
</div>
</div>
<!-- Results -->
<div id="searchResults" style="display:none;">
<div class="card shadow-sm">
<div class="card-header bg-transparent d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-people me-2"></i>Search Results</h5>
<span id="resultCount" class="badge bg-secondary"></span>
</div>
<div class="card-body p-0">
<div id="spinner" class="text-center py-4" style="display:none;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Searching...</span>
</div>
</div>
<div id="noResults" class="text-center py-5" style="display:none;">
<i class="bi bi-inbox display-4 text-muted"></i>
<p class="text-muted mt-2">No eligible users found. Users already holding SuperAdmin are excluded.</p>
</div>
<div class="table-responsive" id="resultsTable" style="display:none;">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Company</th>
<th>Current Role</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody id="resultsBody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Grant Confirmation Modal -->
<div class="modal fade" id="grantModal" tabindex="-1" aria-labelledby="grantModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="grantModalLabel">
<i class="bi bi-shield-plus"></i> Confirm SuperAdmin Grant
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form asp-action="GrantSuperAdmin" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="userId" id="grantUserId" />
<div class="modal-body">
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>Warning:</strong> This user will gain full platform access across all companies.
</div>
<p class="mb-0">
Grant SuperAdmin access to <strong id="grantUserName"></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-plus"></i> Grant SuperAdmin
</button>
</div>
</form>
</div>
</div>
</div>
@section Scripts {
<script>
// Trigger search on Enter key
document.getElementById('searchInput').addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
searchUsers();
}
});
function escapeHtml(str) {
if (!str) return '';
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
async function searchUsers() {
const term = document.getElementById('searchInput').value.trim();
document.getElementById('searchResults').style.display = 'block';
document.getElementById('spinner').style.display = 'block';
document.getElementById('noResults').style.display = 'none';
document.getElementById('resultsTable').style.display = 'none';
document.getElementById('resultCount').textContent = '';
try {
const url = '@Url.Action("SearchUsersForGrant", "PlatformUsers")' + (term ? '?term=' + encodeURIComponent(term) : '');
const response = await fetch(url);
const data = await response.json();
document.getElementById('spinner').style.display = 'none';
if (!data || data.length === 0) {
document.getElementById('noResults').style.display = 'block';
return;
}
document.getElementById('resultCount').textContent = data.length + ' result' + (data.length !== 1 ? 's' : '');
const tbody = document.getElementById('resultsBody');
tbody.innerHTML = '';
data.forEach(function (user) {
const statusBadge = user.isActive
? '<span class="badge bg-success"><i class="bi bi-check-circle"></i> Active</span>'
: '<span class="badge bg-secondary"><i class="bi bi-x-circle"></i> Inactive</span>';
const row = document.createElement('tr');
row.innerHTML =
'<td><strong>' + escapeHtml(user.fullName) + '</strong></td>' +
'<td>' + escapeHtml(user.email) + '</td>' +
'<td><small>' + escapeHtml(user.companyName) + '</small></td>' +
'<td><span class="badge bg-primary"><i class="bi bi-person-gear"></i> ' + escapeHtml(user.companyRole) + '</span></td>' +
'<td>' + statusBadge + '</td>' +
'<td><button type="button" class="btn btn-sm btn-outline-danger" ' +
'onclick="openGrantModal(\'' + escapeHtml(user.id) + '\', \'' + escapeHtml(user.fullName).replace(/'/g, "\\'") + '\')">' +
'<i class="bi bi-shield-plus"></i> Grant</button></td>';
tbody.appendChild(row);
});
document.getElementById('resultsTable').style.display = 'block';
} catch (err) {
document.getElementById('spinner').style.display = 'none';
document.getElementById('noResults').style.display = 'block';
console.error('Search error:', err);
}
}
function openGrantModal(userId, userName) {
document.getElementById('grantUserId').value = userId;
document.getElementById('grantUserName').textContent = userName;
var modal = new bootstrap.Modal(document.getElementById('grantModal'));
modal.show();
}
</script>
}
@@ -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>
}