Initial commit
This commit is contained in:
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// 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>
|
||||
}
|
||||
Reference in New Issue
Block a user