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,893 @@
@model PowderCoating.Application.DTOs.Company.CompanyDto
@{
ViewData["Title"] = Model.CompanyName;
ViewData["PageIcon"] = "bi-building";
var planConfigs = ((IEnumerable<PowderCoating.Core.Entities.SubscriptionPlanConfig>)ViewBag.PlanConfigs)
.OrderBy(c => c.SortOrder).ToList();
var planBadgeColors = planConfigs.Select((c, i) => (c.Plan, i))
.ToDictionary(x => x.Plan, x => x.i switch { 0 => "bg-secondary", 1 => "bg-primary", 2 => "bg-info", _ => "bg-success" });
var planNames = planConfigs.ToDictionary(c => c.Plan, c => c.DisplayName);
string PlanBadge(int plan) => planBadgeColors.TryGetValue(plan, out var c) ? c : "bg-secondary";
string PlanName(int plan) => planNames.TryGetValue(plan, out var n) ? n : plan.ToString();
}
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="d-flex align-items-center gap-2">
@if (!string.IsNullOrEmpty(Model.CompanyCode))
{
<span class="badge bg-secondary">@Model.CompanyCode</span>
}
@if (Model.IsActive)
{
<span class="badge bg-success">Active</span>
}
else
{
<span class="badge bg-danger">Inactive</span>
}
</div>
<div class="btn-group">
<a asp-action="CreateCompanyAdmin" asp-route-id="@Model.Id" class="btn btn-success">
<i class="bi bi-person-plus me-1"></i>Add Admin User
</a>
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-primary">
<i class="bi bi-pencil me-1"></i>Edit
</a>
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back to List
</a>
</div>
</div> @if (TempData["Warning"] != null)
{
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>@TempData["Warning"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
} <!-- Statistics Cards - Desktop -->
<div class="stats-cards-desktop">
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="card shadow-sm">
<div class="card-body text-center">
<i class="bi bi-people text-primary" style="font-size: 2rem;"></i>
<h3 class="mt-2 mb-0">@Model.UserCount</h3>
<p class="text-muted small mb-0">Users</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm">
<div class="card-body text-center">
<i class="bi bi-person-badge text-success" style="font-size: 2rem;"></i>
<h3 class="mt-2 mb-0">@Model.CustomerCount</h3>
<p class="text-muted small mb-0">Customers</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm">
<div class="card-body text-center">
<i class="bi bi-briefcase text-info" style="font-size: 2rem;"></i>
<h3 class="mt-2 mb-0">@Model.JobCount</h3>
<p class="text-muted small mb-0">Jobs</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm">
<div class="card-body text-center">
<i class="bi bi-star text-warning" style="font-size: 2rem;"></i>
<h3 class="mt-2 mb-0">@PlanName(Model.SubscriptionPlan)</h3>
<p class="text-muted small mb-0">Plan</p>
</div>
</div>
</div>
</div>
</div>
<!-- Compact Stats - Mobile -->
<div class="mobile-stats-compact">
<div class="card">
<div class="stats-grid">
<div class="stat-item">
<div class="stat-icon"><i class="bi bi-people text-primary"></i></div>
<div class="stat-value">@Model.UserCount</div>
<div class="stat-label">Users</div>
</div>
<div class="stat-item">
<div class="stat-icon"><i class="bi bi-person-badge text-success"></i></div>
<div class="stat-value">@Model.CustomerCount</div>
<div class="stat-label">Customers</div>
</div>
<div class="stat-item">
<div class="stat-icon"><i class="bi bi-briefcase text-info"></i></div>
<div class="stat-value">@Model.JobCount</div>
<div class="stat-label">Jobs</div>
</div>
<div class="stat-item">
<div class="stat-icon"><i class="bi bi-star text-warning"></i></div>
<div class="stat-value">@PlanName(Model.SubscriptionPlan)</div>
<div class="stat-label">Plan</div>
</div>
</div>
</div>
</div>
<div class="row g-3">
<!-- Company Information -->
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="bi bi-info-circle me-2"></i>Company Information
</h5>
</div>
<div class="card-body">
<table class="table table-sm table-borderless">
<tr>
<th style="width: 40%;">Company Name:</th>
<td>@Model.CompanyName</td>
</tr>
<tr>
<th>Company Code:</th>
<td>@(Model.CompanyCode ?? "N/A")</td>
</tr>
<tr>
<th>Status:</th>
<td>
@if (Model.IsActive)
{
<span class="badge bg-success">Active</span>
}
else
{
<span class="badge bg-danger">Inactive</span>
}
</td>
</tr>
<tr>
<th>Time Zone:</th>
<td>@(Model.TimeZone ?? "America/New_York")</td>
</tr>
</table>
</div>
</div>
</div>
<!-- Contact Information -->
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="bi bi-person-lines-fill me-2"></i>Primary Contact
</h5>
</div>
<div class="card-body">
<table class="table table-sm table-borderless">
<tr>
<th style="width: 40%;">Contact Name:</th>
<td>@Model.PrimaryContactName</td>
</tr>
<tr>
<th>Email:</th>
<td><a href="mailto:@Model.PrimaryContactEmail">@Model.PrimaryContactEmail</a></td>
</tr>
<tr>
<th>Phone:</th>
<td>@(Model.Phone ?? "N/A")</td>
</tr>
</table>
</div>
</div>
</div>
<!-- Address -->
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="bi bi-geo-alt me-2"></i>Address
</h5>
</div>
<div class="card-body">
@if (!string.IsNullOrEmpty(Model.Address))
{
<address>
@Model.Address<br />
@if (!string.IsNullOrEmpty(Model.City) || !string.IsNullOrEmpty(Model.State) || !string.IsNullOrEmpty(Model.ZipCode))
{
<text>@Model.City, @Model.State @Model.ZipCode</text>
}
</address>
}
else
{
<p class="text-muted mb-0">No address provided</p>
}
</div>
</div>
</div>
<!-- Subscription -->
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="bi bi-credit-card me-2"></i>Subscription Details
</h5>
</div>
<div class="card-body">
<table class="table table-sm table-borderless">
<tr>
<th style="width: 40%;">Plan:</th>
<td>
<span class="badge @PlanBadge(Model.SubscriptionPlan)">@PlanName(Model.SubscriptionPlan)</span>
</td>
</tr>
<tr>
<th>Start Date:</th>
<td>@Model.SubscriptionStartDate.ToString("MMMM d, yyyy")</td>
</tr>
<tr>
<th>End Date:</th>
<td>
@if (Model.SubscriptionEndDate.HasValue)
{
@Model.SubscriptionEndDate.Value.ToString("MMMM d, yyyy")
}
else
{
<span class="text-muted">Ongoing</span>
}
</td>
</tr>
</table>
</div>
</div>
</div>
<!-- Audit Information -->
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="bi bi-clock-history me-2"></i>Audit Information
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<table class="table table-sm table-borderless">
<tr>
<th style="width: 30%;">Created:</th>
<td>@Model.CreatedAt.ToString("MMMM d, yyyy h:mm tt")</td>
</tr>
@if (!string.IsNullOrEmpty(Model.CreatedBy))
{
<tr>
<th>Created By:</th>
<td>@Model.CreatedBy</td>
</tr>
}
</table>
</div>
<div class="col-md-6">
@if (Model.UpdatedAt.HasValue)
{
<table class="table table-sm table-borderless">
<tr>
<th style="width: 30%;">Updated:</th>
<td>@Model.UpdatedAt.Value.ToString("MMMM d, yyyy h:mm tt")</td>
</tr>
@if (!string.IsNullOrEmpty(Model.UpdatedBy))
{
<tr>
<th>Updated By:</th>
<td>@Model.UpdatedBy</td>
</tr>
}
</table>
}
</div>
</div>
</div>
</div>
</div>
<!-- Users List -->
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="bi bi-people me-2"></i>Users (@Model.Users.Count)
</h5>
<a asp-action="CreateCompanyAdmin" asp-route-id="@Model.Id" class="btn btn-sm btn-success">
<i class="bi bi-person-plus me-1"></i>Add Admin User
</a>
</div>
<div class="card-body p-0">
@if (Model.Users.Any())
{
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th>Department</th>
<th>Status</th>
<th>Last Login</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var user in Model.Users.OrderBy(u => u.LastName))
{
<tr class="user-row" style="cursor:pointer;"
data-user-id="@user.Id"
data-company-id="@Model.Id"
title="Click to view user details and login history">
<td>
<strong>@user.FullName</strong>
@if (user.CompanyRole == null)
{
<span class="badge bg-warning text-dark ms-1" title="Platform User">
<i class="bi bi-star-fill"></i> SuperAdmin
</span>
}
</td>
<td>
<a href="mailto:@user.Email">@user.Email</a>
</td>
<td>
@if (!string.IsNullOrEmpty(user.CompanyRole))
{
<span class="badge @(user.CompanyRole switch
{
"CompanyAdmin" => "bg-primary",
"Manager" => "bg-info",
"Worker" => "bg-secondary",
_ => "bg-light text-dark"
})">
@user.CompanyRole.Replace("Company", "")
</span>
}
else
{
<span class="text-muted">N/A</span>
}
</td>
<td>@(user.Department ?? "N/A")</td>
<td>
@if (user.IsActive)
{
<span class="badge bg-success">Active</span>
}
else
{
<span class="badge bg-danger">Inactive</span>
}
</td>
<td>
@if (user.LastLoginDate.HasValue)
{
<small class="text-muted">@user.LastLoginDate.Value.ToString("MMM d, yyyy")</small>
}
else
{
<small class="text-muted">Never</small>
}
</td>
<td class="text-end" onclick="event.stopPropagation()">
@if (user.CompanyRole != null)
{
<div class="btn-group btn-group-sm" role="group">
<a asp-controller="CompanyUsers"
asp-action="Edit"
asp-route-id="@user.Id"
asp-route-returnUrl="@Url.Action("Details", "Companies", new { id = Model.Id })"
class="btn btn-outline-primary"
title="Edit User">
<i class="bi bi-pencil"></i>
</a>
<button type="button"
class="btn btn-outline-secondary"
data-bs-toggle="modal"
data-bs-target="#resetPasswordModal"
data-user-id="@user.Id"
data-user-name="@user.FullName"
title="Reset Password (set manually)">
<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>
<form asp-controller="CompanyUsers"
asp-action="ToggleActive"
asp-route-id="@user.Id"
method="post"
class="d-inline">
@Html.AntiForgeryToken()
<button type="submit"
class="btn btn-outline-@(user.IsActive ? "warning" : "success")"
title="@(user.IsActive ? "Deactivate" : "Activate")">
<i class="bi bi-@(user.IsActive ? "pause" : "play")"></i>
</button>
</form>
</div>
}
else
{
<span class="text-muted small">Platform User</span>
}
</td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<div class="text-center py-5">
<i class="bi bi-people" style="font-size: 3rem; color: #ccc;"></i>
<p class="text-muted mt-3 mb-3">No users found for this company.</p>
<a asp-action="CreateCompanyAdmin" asp-route-id="@Model.Id" class="btn btn-primary">
<i class="bi bi-person-plus me-1"></i>Create First Admin User
</a>
</div>
}
</div>
</div>
</div>
<!-- Actions -->
<div class="col-12">
<div class="card shadow-sm border-danger">
<div class="card-header bg-light">
<h5 class="card-title mb-0 text-danger">
<i class="bi bi-exclamation-triangle me-2"></i>Danger Zone
</h5>
</div>
<div class="card-body d-flex flex-column gap-3">
<div class="d-flex justify-content-between align-items-start border rounded p-3 bg-warning bg-opacity-10">
<div>
<h6 class="mb-1 text-warning-emphasis">
<i class="bi bi-fire me-1"></i>Reset All Company Data
</h6>
<p class="text-muted small mb-0">
Permanently deletes <strong>all business data</strong> — customers, vendors, accounts, invoices, bills, jobs, quotes, inventory, catalog items, and more.
The company record, user accounts, and system configuration are preserved.
Use this to wipe a migration and start fresh.
</p>
</div>
<button type="button" class="btn btn-warning ms-3 text-nowrap"
data-bs-toggle="modal" data-bs-target="#resetDataModal">
<i class="bi bi-fire me-1"></i>Reset Data
</button>
</div>
<div class="d-flex justify-content-between align-items-start border rounded p-3 bg-danger bg-opacity-10">
<div>
<h6 class="mb-1 text-danger">
<i class="bi bi-trash me-1"></i>Delete Company
</h6>
<p class="text-muted small mb-0">
Permanently deletes the company <strong>and everything in it</strong>, including all users.
There is no going back.
@if (Model.UserCount > 0)
{
<br /><strong class="text-danger">This company has @Model.UserCount user(s) — remove them first.</strong>
}
</p>
</div>
<button type="button" class="btn btn-danger ms-3 text-nowrap"
data-bs-toggle="modal" data-bs-target="#hardDeleteModal"
@(Model.UserCount > 0 ? "disabled" : "")>
<i class="bi bi-trash me-1"></i>Delete Company
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- User Details Modal -->
<div class="modal fade" tabindex="-1" id="userDetailOffcanvas" aria-labelledby="userDetailOffcanvasLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" style="max-width:520px;">
<div class="modal-content">
<div class="modal-header border-bottom">
<h5 class="modal-title" id="userDetailOffcanvasLabel">
<i class="bi bi-person-circle me-2"></i><span id="oc-name">User Details</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-0">
<!-- Loading spinner -->
<div id="oc-loading" class="d-flex justify-content-center align-items-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading…</span>
</div>
</div>
<!-- Content (hidden until loaded) -->
<div id="oc-content" style="display:none;">
<!-- Profile card -->
<div class="px-3 pt-3 pb-2">
<div class="d-flex align-items-center gap-3 mb-3">
<div class="bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center flex-shrink-0"
style="width:52px;height:52px;">
<i class="bi bi-person-fill text-primary fs-4"></i>
</div>
<div>
<h6 class="mb-0 fw-semibold" id="oc-fullname"></h6>
<small class="text-muted" id="oc-email"></small>
</div>
<span id="oc-status-badge" class="badge ms-auto"></span>
</div>
<table class="table table-sm table-borderless mb-0 small">
<tr><th style="width:38%;" class="text-muted fw-normal">Role</th><td id="oc-role">—</td></tr>
<tr><th class="text-muted fw-normal">Department</th><td id="oc-dept">—</td></tr>
<tr><th class="text-muted fw-normal">Position</th><td id="oc-position">—</td></tr>
<tr><th class="text-muted fw-normal">Phone</th><td id="oc-phone">—</td></tr>
<tr><th class="text-muted fw-normal">Hired</th><td id="oc-hire">—</td></tr>
<tr><th class="text-muted fw-normal">Account created</th><td id="oc-created">—</td></tr>
<tr><th class="text-muted fw-normal">Last login</th><td id="oc-lastlogin">—</td></tr>
<tr><th class="text-muted fw-normal">Email confirmed</th><td id="oc-emailconf">—</td></tr>
</table>
</div>
<hr class="my-0" />
<!-- Login history -->
<div class="px-3 pt-3">
<h6 class="fw-semibold mb-2">
<i class="bi bi-clock-history me-1"></i>Login History
<span class="text-muted fw-normal small" id="oc-log-count"></span>
</h6>
</div>
<div id="oc-no-logs" class="px-3 pb-3 text-muted small" style="display:none;">
No login records found. (The audit log table may have been empty before the fix was applied.)
</div>
<div class="table-responsive" id="oc-log-table-wrap">
<table class="table table-sm table-hover mb-0 small">
<thead class="table-light sticky-top">
<tr>
<th>Time</th>
<th>Event</th>
<th>IP Address</th>
</tr>
</thead>
<tbody id="oc-log-body"></tbody>
</table>
</div>
</div><!-- /oc-content -->
<!-- Error state -->
<div id="oc-error" class="px-3 py-4 text-danger small" style="display:none;">
<i class="bi bi-exclamation-triangle me-1"></i><span id="oc-error-msg">Failed to load user data.</span>
</div>
</div>
</div><!-- /modal-content -->
</div><!-- /modal-dialog -->
</div>
<!-- Reset Data Modal -->
<div class="modal fade" id="resetDataModal" tabindex="-1" aria-labelledby="resetDataModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-warning">
<div class="modal-header bg-warning bg-opacity-10">
<h5 class="modal-title text-warning-emphasis" id="resetDataModalLabel">
<i class="bi bi-fire me-2"></i>Reset All Company Data
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning alert-permanent">
<strong>This will permanently delete:</strong>
<ul class="mb-0 mt-1">
<li>All customers, vendors, catalog items, and inventory</li>
<li>All chart of accounts entries</li>
<li>All invoices, bills, payments, and deposits</li>
<li>All jobs, quotes, and purchase orders</li>
<li>All equipment, shop workers, and maintenance records</li>
<li>All appointments, notifications, and related data</li>
</ul>
</div>
<p><strong>Preserved:</strong> Company record, user accounts, operating costs, preferences, and system lookup tables.</p>
<p class="mb-1">Type <strong>DELETE</strong> to confirm:</p>
<input type="text" id="resetDataConfirmInput" class="form-control" placeholder="Type DELETE here" autocomplete="off" />
</div>
<form asp-action="ResetData" asp-route-id="@Model.Id" method="post" id="resetDataForm">
@Html.AntiForgeryToken()
<input type="hidden" name="confirmation" id="resetDataConfirmHidden" />
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" id="btnResetDataConfirm" class="btn btn-warning" disabled>
<i class="bi bi-fire me-1"></i>Reset All Data
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Hard Delete Company Modal -->
<div class="modal fade" id="hardDeleteModal" tabindex="-1" aria-labelledby="hardDeleteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-danger">
<div class="modal-header bg-danger bg-opacity-10">
<h5 class="modal-title text-danger" id="hardDeleteModalLabel">
<i class="bi bi-trash me-2"></i>Permanently Delete Company
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-danger alert-permanent">
<strong>This will permanently delete the company and everything in it:</strong> all data, all users, all configuration. This cannot be undone.
</div>
<p class="mb-1">Type <strong>DELETE</strong> to confirm:</p>
<input type="text" id="hardDeleteConfirmInput" class="form-control" placeholder="Type DELETE here" autocomplete="off" />
</div>
<form asp-action="HardDelete" asp-route-id="@Model.Id" method="post" id="hardDeleteForm">
@Html.AntiForgeryToken()
<input type="hidden" name="confirmation" id="hardDeleteConfirmHidden" />
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" id="btnHardDeleteConfirm" class="btn btn-danger" disabled>
<i class="bi bi-trash me-1"></i>Permanently Delete Company
</button>
</div>
</form>
</div>
</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 me-2"></i>Reset Password
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="resetPasswordForm" asp-controller="CompanyUsers" asp-action="ResetPassword" method="post">
@Html.AntiForgeryToken()
<input type="hidden" id="resetUserId" name="id" />
<div class="modal-body">
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
You are about to reset the password for <strong id="resetUserName"></strong>.
</div>
<div class="mb-3">
<label for="newPassword" class="form-label">New Password</label>
<input type="password" class="form-control" id="newPassword" name="newPassword" required
minlength="8" placeholder="Enter new password">
<small class="text-muted">
Password must be at least 8 characters with uppercase, lowercase, digit, and special character
</small>
</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-primary">
<i class="bi bi-key me-2"></i>Reset Password
</button>
</div>
</form>
</div>
</div>
</div>
@section Scripts {
<script>
// Reset Data Modal — enable submit only when user types "DELETE"
(function () {
var input = document.getElementById('resetDataConfirmInput');
var hidden = document.getElementById('resetDataConfirmHidden');
var btn = document.getElementById('btnResetDataConfirm');
if (input) {
input.addEventListener('input', function () {
var match = input.value.trim() === 'DELETE';
btn.disabled = !match;
hidden.value = input.value.trim();
});
document.getElementById('resetDataModal').addEventListener('hidden.bs.modal', function () {
input.value = '';
hidden.value = '';
btn.disabled = true;
});
}
})();
// Hard Delete Modal — enable submit only when user types "DELETE"
(function () {
var input = document.getElementById('hardDeleteConfirmInput');
var hidden = document.getElementById('hardDeleteConfirmHidden');
var btn = document.getElementById('btnHardDeleteConfirm');
if (input) {
input.addEventListener('input', function () {
var match = input.value.trim() === 'DELETE';
btn.disabled = !match;
hidden.value = input.value.trim();
});
document.getElementById('hardDeleteModal').addEventListener('hidden.bs.modal', function () {
input.value = '';
hidden.value = '';
btn.disabled = true;
});
}
})();
// Reset Password Modal
var resetPasswordModal = document.getElementById('resetPasswordModal');
if (resetPasswordModal) {
resetPasswordModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var userId = button.getAttribute('data-user-id');
var userName = button.getAttribute('data-user-name');
var modalUserIdInput = resetPasswordModal.querySelector('#resetUserId');
var modalUserName = resetPasswordModal.querySelector('#resetUserName');
modalUserIdInput.value = userId;
modalUserName.textContent = userName;
});
}
// ── User Details Modal ────────────────────────────────────────────────
(function () {
const offcanvasEl = document.getElementById('userDetailOffcanvas');
const oc = new bootstrap.Modal(offcanvasEl);
const loading = document.getElementById('oc-loading');
const content = document.getElementById('oc-content');
const errorDiv = document.getElementById('oc-error');
const errorMsg = document.getElementById('oc-error-msg');
const actionBadgeClass = {
'Login': 'bg-success',
'Login2FABypassed': 'bg-success',
'FailedLogin': 'bg-danger',
'LoginDenied': 'bg-warning text-dark',
'AccountLockedOut': 'bg-danger',
};
const actionLabel = {
'Login': 'Login',
'Login2FABypassed': 'Login (2FA bypassed)',
'FailedLogin': 'Failed login',
'LoginDenied': 'Login denied',
'AccountLockedOut': 'Account locked out',
'SelfServiceAccountDeletion': 'Account deleted',
};
/// Shows the loading spinner and hides all other states.
function showLoading() {
loading.classList.remove('d-none');
content.style.display = 'none';
errorDiv.style.display = 'none';
}
/// Populates the offcanvas with the fetched user data and login history.
function renderUser(data) {
const u = data.user;
document.getElementById('oc-name').textContent = u.fullName;
document.getElementById('oc-fullname').textContent = u.fullName;
document.getElementById('oc-email').textContent = u.email;
const badge = document.getElementById('oc-status-badge');
badge.textContent = u.isActive ? 'Active' : 'Inactive';
badge.className = 'badge ms-auto ' + (u.isActive ? 'bg-success' : 'bg-danger');
document.getElementById('oc-role').textContent = u.companyRole ? u.companyRole.replace('Company', '') : '—';
document.getElementById('oc-dept').textContent = u.department || '—';
document.getElementById('oc-position').textContent = u.position || '—';
document.getElementById('oc-phone').textContent = u.phone || '—';
document.getElementById('oc-hire').textContent = u.hireDate || '—';
document.getElementById('oc-created').textContent = u.createdAt || '—';
document.getElementById('oc-lastlogin').textContent = u.lastLoginDate || 'Never';
document.getElementById('oc-emailconf').innerHTML = u.emailConfirmed
? '<span class="text-success"><i class="bi bi-check-circle-fill me-1"></i>Yes</span>'
: '<span class="text-warning"><i class="bi bi-x-circle-fill me-1"></i>No</span>';
// Login history table
const tbody = document.getElementById('oc-log-body');
const noLogs = document.getElementById('oc-no-logs');
const logWrap = document.getElementById('oc-log-table-wrap');
const countEl = document.getElementById('oc-log-count');
const logs = data.loginHistory;
tbody.innerHTML = '';
if (!logs || logs.length === 0) {
noLogs.style.display = '';
logWrap.style.display = 'none';
countEl.textContent = '';
} else {
noLogs.style.display = 'none';
logWrap.style.display = '';
countEl.textContent = `(${logs.length})`;
logs.forEach(function (log) {
const badgeClass = actionBadgeClass[log.action] || 'bg-secondary';
const label = actionLabel[log.action] || log.action;
const noteHtml = log.note ? `<br><span class="text-muted">${escHtml(log.note)}</span>` : '';
const tr = document.createElement('tr');
tr.innerHTML = `
<td class="text-nowrap">${escHtml(log.timestamp)}</td>
<td><span class="badge ${badgeClass}">${escHtml(label)}</span>${noteHtml}</td>
<td class="text-nowrap text-muted">${escHtml(log.ipAddress)}</td>`;
tbody.appendChild(tr);
});
}
loading.classList.add('d-none');
content.style.display = '';
}
function showError(msg) {
loading.classList.add('d-none');
content.style.display = 'none';
errorMsg.textContent = msg;
errorDiv.style.display = '';
}
function escHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// Wire up every user row
document.querySelectorAll('.user-row').forEach(function (row) {
row.addEventListener('click', function () {
const userId = row.dataset.userId;
const companyId = row.dataset.companyId;
showLoading();
oc.show();
fetch(`/Companies/UserLoginHistory?companyId=${encodeURIComponent(companyId)}&userId=${encodeURIComponent(userId)}`)
.then(function (resp) {
if (!resp.ok) throw new Error('Server returned ' + resp.status);
return resp.json();
})
.then(renderUser)
.catch(function (err) {
showError('Failed to load user data. ' + err.message);
});
});
});
})();
</script>
}