Replace literal Unicode special chars with HTML entities across all 233 views
Sweeps em dashes, en dashes, multiplication signs, ellipses, and curly quotes to their HTML entity equivalents (— – × … ‘ ’) in all .cshtml files, skipping <script> blocks. Prevents encoding corruption from AI tools and Windows encoding mismatches that caused recurring symbol bugs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,831 +0,0 @@
|
||||
@model PowderCoating.Application.DTOs.Company.CompanyDto
|
||||
@using PowderCoating.Web.ViewModels.Platform
|
||||
@using PowderCoating.Web.Controllers
|
||||
|
||||
@{
|
||||
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();
|
||||
|
||||
var healthScore = (int)(ViewBag.HealthScore ?? 0);
|
||||
var healthRisk = (string)(ViewBag.HealthRisk ?? "Healthy");
|
||||
var healthSignals = (List<string>)(ViewBag.HealthSignals ?? new List<string>());
|
||||
var jobs30 = (int)(ViewBag.Jobs30 ?? 0);
|
||||
var jobs90 = (int)(ViewBag.Jobs90 ?? 0);
|
||||
var lastLogin = (DateTime?)(ViewBag.LastLoginDate);
|
||||
var onboarding = (OnboardingProgressRowViewModel?)(ViewBag.Onboarding);
|
||||
|
||||
string ScoreColor(int s) => s >= 75 ? "text-success" : s >= 45 ? "text-warning" : "text-danger";
|
||||
string RiskBadgeClass(string r) => r switch {
|
||||
"Healthy" => "bg-success",
|
||||
"AtRisk" => "bg-warning text-dark",
|
||||
"Critical" => "bg-danger",
|
||||
"NeverActivated" => "bg-secondary",
|
||||
_ => "bg-secondary"
|
||||
};
|
||||
string RiskLabel(string r) => r switch {
|
||||
"Healthy" => "Healthy",
|
||||
"AtRisk" => "At Risk",
|
||||
"Critical" => "Critical",
|
||||
"NeverActivated" => "Never Activated",
|
||||
_ => r
|
||||
};
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<a asp-action="Index" class="text-muted small text-decoration-none">
|
||||
<i class="bi bi-arrow-left me-1"></i>Companies
|
||||
</a>
|
||||
@if (!string.IsNullOrEmpty(Model.CompanyCode))
|
||||
{
|
||||
<span class="badge bg-secondary">@Model.CompanyCode</span>
|
||||
}
|
||||
<span class="badge @(Model.IsActive ? "bg-success" : "bg-danger")">@(Model.IsActive ? "Active" : "Inactive")</span>
|
||||
<span class="badge @RiskBadgeClass(healthRisk)">@RiskLabel(healthRisk)</span>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a asp-action="CreateCompanyAdmin" asp-route-id="@Model.Id" class="btn btn-success btn-sm">
|
||||
<i class="bi bi-person-plus me-1"></i>Add User
|
||||
</a>
|
||||
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-pencil me-1"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (TempData["Warning"] != null)
|
||||
{
|
||||
<div class="alert alert-warning alert-permanent alert-dismissible fade show mb-3" 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>
|
||||
}
|
||||
|
||||
<!-- KPI strip -->
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body text-center py-2">
|
||||
<div class="fs-4 fw-bold text-primary">@Model.UserCount</div>
|
||||
<div class="small text-muted">Users</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body text-center py-2">
|
||||
<div class="fs-4 fw-bold text-success">@Model.CustomerCount</div>
|
||||
<div class="small text-muted">Customers</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body text-center py-2">
|
||||
<div class="fs-4 fw-bold text-info">@Model.JobCount</div>
|
||||
<div class="small text-muted">Jobs</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body text-center py-2">
|
||||
<div class="fs-5 fw-bold @ScoreColor(healthScore)">@healthScore</div>
|
||||
<div class="small text-muted">Health Score</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<ul class="nav nav-tabs mb-3" id="companyDetailTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="tab-overview" data-bs-toggle="tab" data-bs-target="#pane-overview" type="button" role="tab">
|
||||
<i class="bi bi-info-circle me-1"></i>Overview
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-users" data-bs-toggle="tab" data-bs-target="#pane-users" type="button" role="tab">
|
||||
<i class="bi bi-people me-1"></i>Users
|
||||
<span class="badge bg-secondary ms-1">@Model.Users.Count</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-subscription" data-bs-toggle="tab" data-bs-target="#pane-subscription" type="button" role="tab">
|
||||
<i class="bi bi-credit-card me-1"></i>Subscription
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-onboarding" data-bs-toggle="tab" data-bs-target="#pane-onboarding" type="button" role="tab">
|
||||
<i class="bi bi-rocket-takeoff me-1"></i>Onboarding
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-health" data-bs-toggle="tab" data-bs-target="#pane-health" type="button" role="tab">
|
||||
<i class="bi bi-heart-pulse me-1"></i>Health
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="companyDetailTabContent">
|
||||
|
||||
<!-- ── Overview tab ── -->
|
||||
<div class="tab-pane fade show active" id="pane-overview" role="tabpanel">
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0"><i class="bi bi-info-circle me-2"></i>Company Information</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm table-borderless mb-0">
|
||||
<tr><th style="width:40%">Company Name</th><td>@Model.CompanyName</td></tr>
|
||||
<tr><th>Code</th><td>@(Model.CompanyCode ?? "—")</td></tr>
|
||||
<tr><th>Status</th><td><span class="badge @(Model.IsActive ? "bg-success" : "bg-danger")">@(Model.IsActive ? "Active" : "Inactive")</span></td></tr>
|
||||
<tr><th>Time Zone</th><td>@(Model.TimeZone ?? "America/New_York")</td></tr>
|
||||
<tr><th>Created</th><td>@Model.CreatedAt.ToString("MMM d, yyyy h:mm tt")</td></tr>
|
||||
@if (!string.IsNullOrEmpty(Model.CreatedBy))
|
||||
{
|
||||
<tr><th>Created By</th><td>@Model.CreatedBy</td></tr>
|
||||
}
|
||||
@if (Model.UpdatedAt.HasValue)
|
||||
{
|
||||
<tr><th>Last Updated</th><td>@Model.UpdatedAt.Value.ToString("MMM d, yyyy")</td></tr>
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0"><i class="bi bi-person-lines-fill me-2"></i>Primary Contact</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm table-borderless mb-0">
|
||||
<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 ?? "—")</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0"><i class="bi bi-geo-alt me-2"></i>Address</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (!string.IsNullOrEmpty(Model.Address))
|
||||
{
|
||||
<address class="mb-0">
|
||||
@Model.Address<br />
|
||||
@if (!string.IsNullOrEmpty(Model.City) || !string.IsNullOrEmpty(Model.State))
|
||||
{
|
||||
<text>@Model.City, @Model.State @Model.ZipCode</text>
|
||||
}
|
||||
</address>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">No address on file.</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.StripeCustomerId) || !string.IsNullOrEmpty(Model.StripeSubscriptionId))
|
||||
{
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0"><i class="bi bi-stripe me-2"></i>Stripe IDs</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm table-borderless mb-0">
|
||||
@if (!string.IsNullOrEmpty(Model.StripeCustomerId))
|
||||
{
|
||||
<tr><th style="width:40%">Customer</th><td><code class="small">@Model.StripeCustomerId</code></td></tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.StripeSubscriptionId))
|
||||
{
|
||||
<tr><th>Subscription</th><td><code class="small">@Model.StripeSubscriptionId</code></td></tr>
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Users tab ── -->
|
||||
<div class="tab-pane fade" id="pane-users" role="tabpanel">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0"><i class="bi bi-people me-2"></i>Users (@Model.Users.Count)</h6>
|
||||
<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">
|
||||
<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 ?? "—")</td>
|
||||
<td>
|
||||
<span class="badge @(user.IsActive ? "bg-success" : "bg-danger")">
|
||||
@(user.IsActive ? "Active" : "Inactive")
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
@(user.LastLoginDate.HasValue
|
||||
? user.LastLoginDate.Value.ToString("MMM d, yyyy")
|
||||
: "Never")
|
||||
</small>
|
||||
</td>
|
||||
<td class="text-end" onclick="event.stopPropagation()">
|
||||
@if (user.CompanyRole != null)
|
||||
{
|
||||
<div class="btn-group btn-group-sm">
|
||||
<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">
|
||||
<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">
|
||||
<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 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>
|
||||
|
||||
<!-- ── Subscription tab ── -->
|
||||
<div class="tab-pane fade" id="pane-subscription" role="tabpanel">
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0"><i class="bi bi-credit-card me-2"></i>Subscription</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm table-borderless mb-3">
|
||||
<tr>
|
||||
<th style="width:40%">Plan</th>
|
||||
<td><span class="badge @PlanBadge(Model.SubscriptionPlan)">@PlanName(Model.SubscriptionPlan)</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<td>
|
||||
@{
|
||||
var (ssBadge, ssLabel) = Model.SubscriptionStatus switch {
|
||||
PowderCoating.Core.Enums.SubscriptionStatus.Active => ("bg-success", "Active"),
|
||||
PowderCoating.Core.Enums.SubscriptionStatus.GracePeriod => ("bg-warning text-dark", "Grace Period"),
|
||||
PowderCoating.Core.Enums.SubscriptionStatus.Expired => ("bg-danger", "Expired"),
|
||||
PowderCoating.Core.Enums.SubscriptionStatus.Canceled => ("bg-secondary", "Canceled"),
|
||||
PowderCoating.Core.Enums.SubscriptionStatus.Inactive => ("bg-secondary", "Inactive"),
|
||||
_ => ("bg-secondary", Model.SubscriptionStatus.ToString())
|
||||
};
|
||||
}
|
||||
<span class="badge @ssBadge">@ssLabel</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)
|
||||
{
|
||||
var daysLeft = (int)(Model.SubscriptionEndDate.Value.Date - DateTime.UtcNow.Date).TotalDays;
|
||||
<span class="@(daysLeft < 0 ? "text-danger" : daysLeft <= 14 ? "text-warning" : "")">
|
||||
@Model.SubscriptionEndDate.Value.ToString("MMMM d, yyyy")
|
||||
@if (daysLeft >= 0 && daysLeft <= 30) { <small class="text-muted">(in @daysLeft days)</small> }
|
||||
@if (daysLeft < 0) { <small>(expired @(-daysLeft)d ago)</small> }
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Ongoing</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a asp-controller="SubscriptionManagement" asp-action="Manage" asp-route-id="@Model.Id"
|
||||
class="btn btn-primary">
|
||||
<i class="bi bi-sliders me-1"></i>Manage Subscription & Features
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0"><i class="bi bi-speedometer2 me-2"></i>Activity Snapshot</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm table-borderless mb-0">
|
||||
<tr>
|
||||
<th style="width:55%">Last Login</th>
|
||||
<td>
|
||||
@if (lastLogin.HasValue)
|
||||
{
|
||||
var days = (int)(DateTime.UtcNow - lastLogin.Value).TotalDays;
|
||||
<span class="@(days >= 60 ? "text-danger" : days >= 30 ? "text-warning" : "text-success")">
|
||||
@lastLogin.Value.ToString("MMM d, yyyy") (@days days ago)
|
||||
</span>
|
||||
}
|
||||
else { <span class="text-muted">Never</span> }
|
||||
</td>
|
||||
</tr>
|
||||
<tr><th>Jobs (last 30d)</th><td>@jobs30</td></tr>
|
||||
<tr><th>Jobs (last 90d)</th><td>@jobs90</td></tr>
|
||||
<tr><th>Jobs total</th><td>@Model.JobCount</td></tr>
|
||||
<tr><th>Customers</th><td>@Model.CustomerCount</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Onboarding tab ── -->
|
||||
<div class="tab-pane fade" id="pane-onboarding" role="tabpanel">
|
||||
@if (onboarding != null)
|
||||
{
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0"><i class="bi bi-rocket-takeoff me-2"></i>Setup Progress</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
@{
|
||||
var (obBadge, obLabel) = onboarding.Status switch {
|
||||
OnboardingStatus.Complete => ("bg-success", "Complete"),
|
||||
OnboardingStatus.InProgress => ("bg-primary", "In Progress"),
|
||||
OnboardingStatus.Dismissed => ("bg-secondary", "Dismissed"),
|
||||
_ => ("bg-light text-dark border", "Not Started")
|
||||
};
|
||||
}
|
||||
<span>
|
||||
<span class="text-muted small me-1">Status:</span>
|
||||
<span class="badge @obBadge">@obLabel</span>
|
||||
</span>
|
||||
@if (!string.IsNullOrEmpty(onboarding.OnboardingPath))
|
||||
{
|
||||
<span>
|
||||
<span class="text-muted small me-1">Path:</span>
|
||||
<span class="badge bg-info">@onboarding.OnboardingPath</span>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<table class="table table-sm table-borderless mb-3">
|
||||
<tr>
|
||||
<th style="width:55%">Setup Wizard</th>
|
||||
<td>
|
||||
@if (onboarding.WizardCompleted)
|
||||
{
|
||||
<span class="text-success"><i class="bi bi-check-circle-fill me-1"></i>Complete</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted"><i class="bi bi-hourglass me-1"></i>Pending</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Milestones</th>
|
||||
<td>@onboarding.StepsCompleted / @onboarding.TotalSteps</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="progress mb-3" style="height:8px;">
|
||||
@{
|
||||
var pct = onboarding.TotalSteps > 0
|
||||
? (int)(onboarding.StepsCompleted * 100.0 / onboarding.TotalSteps)
|
||||
: 0;
|
||||
var barClass = pct == 100 ? "bg-success" : pct > 0 ? "bg-primary" : "bg-secondary";
|
||||
}
|
||||
<div class="progress-bar @barClass" style="width:@pct%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0"><i class="bi bi-flag me-2"></i>Milestone Dates</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm table-borderless mb-0">
|
||||
<tr>
|
||||
<th style="width:55%">First Job / Quote</th>
|
||||
<td>
|
||||
@{
|
||||
var firstActivity = onboarding.FirstJobCreatedAt ?? onboarding.FirstQuoteCreatedAt;
|
||||
}
|
||||
@(firstActivity.HasValue ? firstActivity.Value.ToString("MMM d, yyyy") : "—")
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>First Invoice</th>
|
||||
<td>@(onboarding.FirstInvoiceCreatedAt.HasValue ? onboarding.FirstInvoiceCreatedAt.Value.ToString("MMM d, yyyy") : "—")</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Workflow Completed</th>
|
||||
<td>@(onboarding.FirstWorkflowCompletedAt.HasValue ? onboarding.FirstWorkflowCompletedAt.Value.ToString("MMM d, yyyy") : "—")</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Widget Dismissed</th>
|
||||
<td>@(onboarding.GuidedActivationDismissedAt.HasValue ? onboarding.GuidedActivationDismissedAt.Value.ToString("MMM d, yyyy") : "—")</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-muted">Onboarding data not available.</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- ── Health tab ── -->
|
||||
<div class="tab-pane fade" id="pane-health" role="tabpanel">
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0"><i class="bi bi-heart-pulse me-2"></i>Health Score</h6>
|
||||
</div>
|
||||
<div class="card-body text-center py-4">
|
||||
<div class="display-4 fw-bold @ScoreColor(healthScore)">@healthScore</div>
|
||||
<div class="mt-1 mb-3">
|
||||
<span class="badge @RiskBadgeClass(healthRisk) fs-6">@RiskLabel(healthRisk)</span>
|
||||
</div>
|
||||
<div class="progress" style="height:10px;">
|
||||
<div class="progress-bar @(healthScore >= 75 ? "bg-success" : healthScore >= 45 ? "bg-warning" : "bg-danger")"
|
||||
style="width:@healthScore%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0"><i class="bi bi-flag me-2"></i>Risk Signals</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (healthSignals.Any())
|
||||
{
|
||||
<ul class="list-group list-group-flush">
|
||||
@foreach (var signal in healthSignals)
|
||||
{
|
||||
<li class="list-group-item d-flex align-items-center gap-2 px-0">
|
||||
<i class="bi bi-exclamation-triangle-fill text-warning"></i>
|
||||
@signal
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center py-3 text-success">
|
||||
<i class="bi bi-check-circle-fill fs-3"></i>
|
||||
<p class="mb-0 mt-2">No risk signals detected.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<hr class="my-3" />
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<a asp-controller="CompanyHealth" asp-action="Index"
|
||||
asp-route-search="@Model.PrimaryContactEmail"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-binoculars me-1"></i>View in Health Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /tab-content -->
|
||||
|
||||
<!-- Danger Zone (outside tabs — always present) -->
|
||||
<div class="card shadow-sm border-danger mt-4">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="card-title mb-0 text-danger">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>Danger Zone
|
||||
</h6>
|
||||
</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 all business data — customers, jobs, quotes, invoices, inventory, and more.
|
||||
The company record, users, and settings 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 and everything in it. 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>
|
||||
|
||||
<!-- 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">
|
||||
<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>
|
||||
<div id="oc-content" style="display:none;">
|
||||
<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" />
|
||||
<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.
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
</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"></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>
|
||||
</ul>
|
||||
</div>
|
||||
<p><strong>Preserved:</strong> Company record, users, operating costs, preferences, and 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-outline-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 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"></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> 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-outline-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
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reset Password Modal -->
|
||||
<div class="modal fade" id="resetPasswordModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-key me-2"></i>Reset Password</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></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 alert-permanent">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
Resetting 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">
|
||||
<small class="text-muted">Min 8 characters with uppercase, lowercase, digit, and special character.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-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 src="~/js/companies-details.js" asp-append-version="true"></script>
|
||||
}
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
@model PowderCoating.Application.DTOs.Company.UpdateCompanyDto
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Edit Company";
|
||||
ViewData["PageIcon"] = "bi-building-gear";
|
||||
var planConfigs = ((IEnumerable<PowderCoating.Core.Entities.SubscriptionPlanConfig>)ViewBag.PlanConfigs)
|
||||
.OrderBy(c => c.SortOrder).ToList();
|
||||
}
|
||||
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
<div class="d-flex justify-content-between mb-4">
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Back to List
|
||||
</a>
|
||||
<a asp-controller="SubscriptionManagement" asp-action="Manage" asp-route-id="@Model.Id"
|
||||
class="btn btn-outline-info">
|
||||
<i class="bi bi-credit-card me-1"></i>Subscription & Features
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form asp-action="Edit" method="post">
|
||||
<input type="hidden" asp-for="Id" />
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<h5 class="card-title mb-3 pb-2 border-bottom">Company Information</h5>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label asp-for="CompanyName" class="form-label">Company Name *</label>
|
||||
<input asp-for="CompanyName" class="form-control" />
|
||||
<span asp-validation-for="CompanyName" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="CompanyCode" class="form-label">Company Code</label>
|
||||
<input asp-for="CompanyCode" class="form-control" maxlength="10" />
|
||||
<span asp-validation-for="CompanyCode" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="card-title mb-3 pb-2 border-bottom">Primary Contact</h5>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label asp-for="PrimaryContactName" class="form-label">Contact Name *</label>
|
||||
<input asp-for="PrimaryContactName" class="form-control" />
|
||||
<span asp-validation-for="PrimaryContactName" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="PrimaryContactEmail" class="form-label">Contact Email *</label>
|
||||
<input asp-for="PrimaryContactEmail" class="form-control" type="email" />
|
||||
<span asp-validation-for="PrimaryContactEmail" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Phone" class="form-label">Phone</label>
|
||||
<input asp-for="Phone" class="form-control" />
|
||||
<span asp-validation-for="Phone" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="card-title mb-3 pb-2 border-bottom">Address</h5>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12">
|
||||
<label asp-for="Address" class="form-label">Street Address</label>
|
||||
<input asp-for="Address" class="form-control" />
|
||||
<span asp-validation-for="Address" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="City" class="form-label">City</label>
|
||||
<input asp-for="City" class="form-control" />
|
||||
<span asp-validation-for="City" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="State" class="form-label">State</label>
|
||||
<input asp-for="State" class="form-control" maxlength="2" />
|
||||
<span asp-validation-for="State" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="ZipCode" class="form-label">Zip Code</label>
|
||||
<input asp-for="ZipCode" class="form-control" />
|
||||
<span asp-validation-for="ZipCode" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="card-title mb-3 pb-2 border-bottom">Subscription Details</h5>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<label asp-for="SubscriptionPlan" class="form-label">Plan</label>
|
||||
<select asp-for="SubscriptionPlan" class="form-select">
|
||||
@foreach (var plan in planConfigs)
|
||||
{
|
||||
<option value="@plan.Plan">@plan.DisplayName</option>
|
||||
}
|
||||
</select>
|
||||
<span asp-validation-for="SubscriptionPlan" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="SubscriptionStartDate" class="form-label">Start Date</label>
|
||||
<input asp-for="SubscriptionStartDate" class="form-control" type="date" />
|
||||
<span asp-validation-for="SubscriptionStartDate" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="SubscriptionEndDate" class="form-label">End Date</label>
|
||||
<input asp-for="SubscriptionEndDate" class="form-control" type="date" />
|
||||
<span asp-validation-for="SubscriptionEndDate" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="TimeZone" class="form-label">Time Zone</label>
|
||||
<select asp-for="TimeZone" class="form-select">
|
||||
<option value="America/New_York">Eastern Time (ET)</option>
|
||||
<option value="America/Chicago">Central Time (CT)</option>
|
||||
<option value="America/Denver">Mountain Time (MT)</option>
|
||||
<option value="America/Phoenix">Arizona Time (MT - No DST)</option>
|
||||
<option value="America/Los_Angeles">Pacific Time (PT)</option>
|
||||
<option value="America/Anchorage">Alaska Time (AKT)</option>
|
||||
<option value="Pacific/Honolulu">Hawaii Time (HT)</option>
|
||||
</select>
|
||||
<span asp-validation-for="TimeZone" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch mt-4">
|
||||
<input asp-for="IsActive" class="form-check-input" type="checkbox" />
|
||||
<label asp-for="IsActive" class="form-check-label">Company Active</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="card-title mb-3 pb-2 border-bottom">Feature Overrides</h5>
|
||||
<p class="text-muted small mb-3">Override plan-level feature access for this company. Leave blank (—) to inherit from the subscription plan.</p>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Online Payments</label>
|
||||
<select asp-for="OnlinePaymentsOverride" class="form-select">
|
||||
<option value="">— Use plan default —</option>
|
||||
<option value="true">Force Enable</option>
|
||||
<option value="false">Force Disable</option>
|
||||
</select>
|
||||
<div class="form-text">Stripe Connect invoice payments. Still requires the company to connect their Stripe account.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Accounting Module</label>
|
||||
<select asp-for="AccountingOverride" class="form-select">
|
||||
<option value="">— Use plan default —</option>
|
||||
<option value="true">Force Enable</option>
|
||||
<option value="false">Force Disable</option>
|
||||
</select>
|
||||
<div class="form-text">Chart of Accounts, Bills, Expenses, and Accounting Export.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="card-title mb-3 pb-2 border-bottom">SMS Override</h5>
|
||||
<p class="text-muted small mb-3">Use this to immediately cut off SMS for a company — for example if they are sending abusive messages or have a billing dispute. This overrides the plan entitlement and the company's own opt-in setting.</p>
|
||||
<div class="mb-4">
|
||||
<div class="form-check form-switch">
|
||||
<input asp-for="SmsDisabledByAdmin" class="form-check-input" type="checkbox" role="switch" id="SmsDisabledByAdmin" />
|
||||
<label asp-for="SmsDisabledByAdmin" class="form-check-label fw-medium text-danger">Force-disable SMS for this company</label>
|
||||
</div>
|
||||
<div class="form-text">When checked, no outbound SMS will be sent for this company regardless of their plan or own settings.</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 justify-content-end">
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-save me-1"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{
|
||||
await Html.RenderPartialAsync("_ValidationScriptsPartial");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,517 +0,0 @@
|
||||
@model List<PowderCoating.Application.DTOs.Company.CompanyListDto>
|
||||
|
||||
@section Styles {
|
||||
<style>
|
||||
[data-bs-theme="dark"] a.text-dark { color: var(--bs-body-color) !important; }
|
||||
[data-bs-theme="dark"] .bi-building[style*="color:#ccc"] { color: var(--bs-secondary-color) !important; }
|
||||
</style>
|
||||
}
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Companies";
|
||||
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();
|
||||
|
||||
var searchTerm = (string?)ViewBag.SearchTerm;
|
||||
var sortColumn = (string)(ViewBag.SortColumn ?? "CompanyName");
|
||||
var sortDirection = (string)(ViewBag.SortDirection ?? "asc");
|
||||
var pageNumber = (int)(ViewBag.PageNumber ?? 1);
|
||||
var pageSize = (int)(ViewBag.PageSize ?? 25);
|
||||
var totalPages = (int)(ViewBag.TotalPages ?? 1);
|
||||
var totalCount = (int)(ViewBag.TotalCount ?? 0);
|
||||
var impersonatingId = (int?)(ViewBag.ImpersonatingCompanyId);
|
||||
var showChurned = (bool)(ViewBag.ShowChurned ?? false);
|
||||
var churnedCount = (int)(ViewBag.ChurnedCount ?? 0);
|
||||
|
||||
string SortLink(string col)
|
||||
{
|
||||
var dir = (sortColumn == col && sortDirection == "asc") ? "desc" : "asc";
|
||||
return Url.Action("Index", new { searchTerm, sortColumn = col, sortDirection = dir, pageNumber = 1, pageSize, showChurned })!;
|
||||
}
|
||||
|
||||
string SortIcon(string col)
|
||||
{
|
||||
if (sortColumn != col) return "bi-chevron-expand text-muted";
|
||||
return sortDirection == "asc" ? "bi-chevron-up" : "bi-chevron-down";
|
||||
}
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-end mb-4">
|
||||
<a asp-action="Create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-1"></i>Create New Company
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body py-2">
|
||||
<form asp-action="Index" method="get" class="row g-2 align-items-end">
|
||||
<input type="hidden" name="sortColumn" value="@sortColumn" />
|
||||
<input type="hidden" name="sortDirection" value="@sortDirection" />
|
||||
<input type="hidden" name="pageSize" value="@pageSize" />
|
||||
<input type="hidden" name="showChurned" value="@showChurned.ToString().ToLower()" />
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||
<input type="text" name="searchTerm" class="form-control"
|
||||
placeholder="Search by name, code, email, phone…"
|
||||
value="@searchTerm" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-search"></i></button>
|
||||
@if (!string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
<a asp-action="Index" asp-route-sortColumn="@sortColumn"
|
||||
asp-route-sortDirection="@sortDirection" asp-route-pageSize="@pageSize"
|
||||
class="btn btn-outline-secondary ms-1">Clear</a>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (churnedCount > 0 && !showChurned)
|
||||
{
|
||||
<div class="alert alert-secondary alert-permanent d-flex align-items-center gap-2 mb-3 py-2">
|
||||
<i class="bi bi-eye-slash text-muted"></i>
|
||||
<span class="small"><strong>@churnedCount</strong> churned @(churnedCount == 1 ? "account" : "accounts") (expired or canceled 14+ days ago) hidden.</span>
|
||||
<a href="@Url.Action("Index", new { searchTerm, sortColumn, sortDirection, pageNumber = 1, pageSize, showChurned = true })"
|
||||
class="btn btn-sm btn-outline-secondary ms-auto py-0">Show churned</a>
|
||||
</div>
|
||||
}
|
||||
else if (showChurned && churnedCount > 0)
|
||||
{
|
||||
<div class="alert alert-warning alert-permanent d-flex align-items-center gap-2 mb-3 py-2">
|
||||
<i class="bi bi-eye text-warning"></i>
|
||||
<span class="small">Showing all accounts including <strong>@churnedCount</strong> churned.</span>
|
||||
<a href="@Url.Action("Index", new { searchTerm, sortColumn, sortDirection, pageNumber = 1, pageSize, showChurned = false })"
|
||||
class="btn btn-sm btn-outline-secondary ms-auto py-0">Hide churned</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
@if (Model != null && Model.Any())
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>
|
||||
<a href="@SortLink("CompanyName")" class="text-decoration-none text-dark d-flex align-items-center gap-1">
|
||||
Company Name <i class="bi @SortIcon("CompanyName")"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>Code</th>
|
||||
<th>Contact Email</th>
|
||||
<th>Phone</th>
|
||||
<th>
|
||||
<a href="@SortLink("Plan")" class="text-decoration-none text-dark d-flex align-items-center gap-1">
|
||||
Plan <i class="bi @SortIcon("Plan")"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>Health</th>
|
||||
<th>Users</th>
|
||||
<th>Setup Wizard</th>
|
||||
<th>
|
||||
<a href="@SortLink("Status")" class="text-decoration-none text-dark d-flex align-items-center gap-1">
|
||||
Status <i class="bi @SortIcon("Status")"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a href="@SortLink("Created")" class="text-decoration-none text-dark d-flex align-items-center gap-1">
|
||||
Created <i class="bi @SortIcon("Created")"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var company in Model)
|
||||
{
|
||||
var isImpersonating = impersonatingId.HasValue && impersonatingId.Value == company.Id;
|
||||
var detailsUrl = Url.Action("Details", new { id = company.Id });
|
||||
<tr class="@(isImpersonating ? "table-warning" : "")" style="cursor:pointer"
|
||||
onclick="window.location='@detailsUrl'">
|
||||
<td>
|
||||
<strong>@company.CompanyName</strong>
|
||||
@if (isImpersonating)
|
||||
{
|
||||
<span class="badge bg-warning text-dark ms-1">
|
||||
<i class="bi bi-eye-fill me-1"></i>Active
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (!string.IsNullOrEmpty(company.CompanyCode))
|
||||
{
|
||||
<span class="badge bg-secondary">@company.CompanyCode</span>
|
||||
}
|
||||
</td>
|
||||
<td>@company.PrimaryContactEmail</td>
|
||||
<td>@company.Phone</td>
|
||||
<td>
|
||||
<span class="badge @PlanBadge(company.SubscriptionPlan)">@PlanName(company.SubscriptionPlan)</span>
|
||||
</td>
|
||||
<td>
|
||||
@{
|
||||
var (hBadge, hLabel) = company.HealthRisk switch {
|
||||
"Healthy" => ("bg-success-subtle text-success-emphasis border border-success-subtle", "Healthy"),
|
||||
"AtRisk" => ("bg-warning-subtle text-warning-emphasis border border-warning-subtle", "At Risk"),
|
||||
"Critical" => ("bg-danger-subtle text-danger-emphasis border border-danger-subtle", "Critical"),
|
||||
"NeverActivated" => ("bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle", "Never Active"),
|
||||
_ => ("bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle", company.HealthRisk)
|
||||
};
|
||||
}
|
||||
<span class="badge @hBadge" title="Score: @company.HealthScore">@hLabel</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-primary rounded-pill">@company.UserCount</span>
|
||||
</td>
|
||||
<td>
|
||||
@if (company.WizardCompleted)
|
||||
{
|
||||
var tooltip = company.WizardCompletedByName != null
|
||||
? $"Completed by {company.WizardCompletedByName}"
|
||||
+ (company.WizardCompletedAt.HasValue
|
||||
? $" on {company.WizardCompletedAt.Value.Tz(ViewBag.CompanyTimeZone as string):MMM d, yyyy}"
|
||||
: "")
|
||||
: "Completed";
|
||||
<span class="badge bg-success" title="@tooltip" data-bs-toggle="tooltip">
|
||||
<i class="bi bi-check-circle-fill me-1"></i>Done
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-light text-muted border">
|
||||
<i class="bi bi-hourglass me-1"></i>Pending
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (company.IsActive)
|
||||
{
|
||||
<span class="badge bg-success">Active</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-danger">Inactive</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">@company.CreatedAt.ToString("MMM d, yyyy")</small>
|
||||
</td>
|
||||
<td class="text-end" onclick="event.stopPropagation()">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a asp-action="Details" asp-route-id="@company.Id"
|
||||
class="btn btn-outline-primary" title="View Details">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<a asp-action="Edit" asp-route-id="@company.Id"
|
||||
class="btn btn-outline-secondary" title="Edit">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<a asp-controller="SubscriptionManagement" asp-action="Manage" asp-route-id="@company.Id"
|
||||
class="btn btn-outline-info" title="Manage Subscription & Features">
|
||||
<i class="bi bi-credit-card"></i>
|
||||
</a>
|
||||
<form asp-action="ToggleActive" asp-route-id="@company.Id"
|
||||
method="post" class="d-inline">
|
||||
@Html.AntiForgeryToken()
|
||||
<button type="submit"
|
||||
class="btn @(company.IsActive ? "btn-outline-warning" : "btn-outline-success")"
|
||||
title="@(company.IsActive ? "Deactivate" : "Activate")">
|
||||
<i class="bi bi-@(company.IsActive ? "pause" : "play")"></i>
|
||||
</button>
|
||||
</form>
|
||||
@if (isImpersonating)
|
||||
{
|
||||
<form asp-action="StopImpersonating" method="post" class="d-inline">
|
||||
@Html.AntiForgeryToken()
|
||||
<button type="submit" class="btn btn-warning" title="Stop Impersonating">
|
||||
<i class="bi bi-x-circle"></i>
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form asp-action="StartImpersonating" method="post" class="d-inline">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="companyId" value="@company.Id" />
|
||||
<button type="submit" class="btn btn-outline-dark" title="Impersonate this company">
|
||||
<i class="bi bi-person-fill-gear"></i>
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
<button type="button"
|
||||
class="btn btn-outline-danger"
|
||||
title="Delete Company"
|
||||
onclick="event.stopPropagation(); openDeleteModal(@company.Id, '@Html.Raw(company.CompanyName.Replace("'", "\\'"))', @company.UserCount, @company.JobCount, @company.QuoteCount, @company.CustomerCount)">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</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 company in Model)
|
||||
{
|
||||
var isImpersonating = impersonatingId.HasValue && impersonatingId.Value == company.Id;
|
||||
<a href="@Url.Action("Details", new { id = company.Id })" class="mobile-data-card text-decoration-none">
|
||||
<div class="mobile-card-header">
|
||||
<div class="mobile-card-icon bg-primary"><i class="bi bi-building"></i></div>
|
||||
<div class="mobile-card-title">
|
||||
<h6>@company.CompanyName</h6>
|
||||
<small class="text-muted">@company.PrimaryContactEmail</small>
|
||||
</div>
|
||||
@if (company.IsActive)
|
||||
{
|
||||
<span class="badge bg-success">Active</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-danger">Inactive</span>
|
||||
}
|
||||
</div>
|
||||
<div class="mobile-card-body">
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Plan</span>
|
||||
<span class="mobile-card-value"><span class="badge @PlanBadge(company.SubscriptionPlan)">@PlanName(company.SubscriptionPlan)</span></span>
|
||||
</div>
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Users</span>
|
||||
<span class="mobile-card-value"><span class="badge bg-primary rounded-pill">@company.UserCount</span></span>
|
||||
</div>
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Created</span>
|
||||
<span class="mobile-card-value">@company.CreatedAt.ToString("MMM d, yyyy")</span>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(company.CompanyCode))
|
||||
{
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Code</span>
|
||||
<span class="mobile-card-value"><span class="badge bg-secondary">@company.CompanyCode</span></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="mobile-card-footer">
|
||||
<span class="btn btn-sm btn-outline-primary">View →</span>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if (totalPages > 1)
|
||||
{
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<div class="text-muted small">
|
||||
Showing @((pageNumber - 1) * pageSize + 1)–@(Math.Min(pageNumber * pageSize, totalCount)) of @totalCount companies
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div>
|
||||
<select class="form-select form-select-sm" onchange="changePageSize(this.value)">
|
||||
@foreach (var size in new[] { 10, 25, 50, 100 })
|
||||
{
|
||||
<option value="@size" selected="@(pageSize == size)">@size per page</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
<li class="page-item @(pageNumber == 1 ? "disabled" : "")">
|
||||
<a class="page-link" href="@Url.Action("Index", new { searchTerm, sortColumn, sortDirection, pageNumber = pageNumber - 1, pageSize, showChurned })">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
@for (int p = Math.Max(1, pageNumber - 2); p <= Math.Min(totalPages, pageNumber + 2); p++)
|
||||
{
|
||||
<li class="page-item @(p == pageNumber ? "active" : "")">
|
||||
<a class="page-link" href="@Url.Action("Index", new { searchTerm, sortColumn, sortDirection, pageNumber = p, pageSize, showChurned })">@p</a>
|
||||
</li>
|
||||
}
|
||||
<li class="page-item @(pageNumber == totalPages ? "disabled" : "")">
|
||||
<a class="page-link" href="@Url.Action("Index", new { searchTerm, sortColumn, sortDirection, pageNumber = pageNumber + 1, pageSize, showChurned })">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-building text-secondary" style="font-size: 3rem;"></i>
|
||||
<p class="text-muted mt-3">
|
||||
@if (!string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
<text>No companies match "<strong>@searchTerm</strong>".</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>No companies found.</text>
|
||||
}
|
||||
</p>
|
||||
@if (string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
<a asp-action="Create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-1"></i>Create Your First Company
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Company Modal -->
|
||||
<div class="modal fade" id="deleteCompanyModal" tabindex="-1" aria-labelledby="deleteCompanyModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content border-0 shadow-lg">
|
||||
<div class="modal-header bg-danger text-white">
|
||||
<h5 class="modal-title" id="deleteCompanyModalLabel">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>Delete Company
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<p class="mb-3">
|
||||
You are about to delete <strong id="modal-company-name"></strong>. This company has the following data:
|
||||
</p>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-3">
|
||||
<div class="card border text-center p-2">
|
||||
<div class="fs-4 fw-bold text-primary" id="modal-user-count">0</div>
|
||||
<small class="text-muted">Users</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="card border text-center p-2">
|
||||
<div class="fs-4 fw-bold text-success" id="modal-job-count">0</div>
|
||||
<small class="text-muted">Jobs</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="card border text-center p-2">
|
||||
<div class="fs-4 fw-bold text-info" id="modal-quote-count">0</div>
|
||||
<small class="text-muted">Quotes</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="card border text-center p-2">
|
||||
<div class="fs-4 fw-bold text-warning" id="modal-customer-count">0</div>
|
||||
<small class="text-muted">Customers</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<!-- Soft Delete Section -->
|
||||
<div class="mb-4">
|
||||
<h6 class="fw-bold text-warning"><i class="bi bi-pause-circle me-2"></i>Soft Delete (Deactivate)</h6>
|
||||
<p class="text-muted small mb-2">
|
||||
The company and all users will be <strong>deactivated</strong> but data is preserved.
|
||||
This is reversible and can be undone by an administrator.
|
||||
</p>
|
||||
<form id="softDeleteForm" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="id" id="soft-delete-id" />
|
||||
<button type="submit" class="btn btn-warning">
|
||||
<i class="bi bi-pause-circle me-1"></i>Deactivate Company
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<!-- Hard Delete Section -->
|
||||
<div>
|
||||
<h6 class="fw-bold text-danger"><i class="bi bi-fire me-2"></i>Hard Delete (Permanent)</h6>
|
||||
<p class="text-muted small mb-2">
|
||||
<strong class="text-danger">This cannot be undone.</strong>
|
||||
All company data — users, jobs, quotes, customers, invoices, and everything else — will be
|
||||
<strong>permanently and irreversibly deleted</strong> from the database.
|
||||
</p>
|
||||
<div class="alert alert-danger alert-permanent py-2 mb-3">
|
||||
<i class="bi bi-exclamation-octagon-fill me-2"></i>
|
||||
Type <strong>DELETE</strong> below to enable permanent deletion.
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="text"
|
||||
id="hard-delete-confirmation"
|
||||
class="form-control"
|
||||
placeholder="Type DELETE to confirm"
|
||||
autocomplete="off"
|
||||
oninput="validateHardDelete(this.value)" />
|
||||
</div>
|
||||
<form id="hardDeleteForm" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="id" id="hard-delete-id" />
|
||||
<input type="hidden" name="confirmation" value="DELETE" />
|
||||
<button type="submit" id="hard-delete-btn" class="btn btn-danger" disabled>
|
||||
<i class="bi bi-trash-fill me-1"></i>Permanently Delete Everything
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function changePageSize(size) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('pageSize', size);
|
||||
url.searchParams.set('pageNumber', '1');
|
||||
url.searchParams.set('showChurned', '@showChurned.ToString().ToLower()');
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
function openDeleteModal(id, name, users, jobs, quotes, customers) {
|
||||
document.getElementById('modal-company-name').textContent = name;
|
||||
document.getElementById('modal-user-count').textContent = users;
|
||||
document.getElementById('modal-job-count').textContent = jobs;
|
||||
document.getElementById('modal-quote-count').textContent = quotes;
|
||||
document.getElementById('modal-customer-count').textContent = customers;
|
||||
|
||||
document.getElementById('soft-delete-id').value = id;
|
||||
document.getElementById('hard-delete-id').value = id;
|
||||
|
||||
document.getElementById('softDeleteForm').action = '/Companies/SoftDelete/' + id;
|
||||
document.getElementById('hardDeleteForm').action = '/Companies/HardDelete/' + id;
|
||||
|
||||
// Reset hard delete input
|
||||
document.getElementById('hard-delete-confirmation').value = '';
|
||||
document.getElementById('hard-delete-btn').disabled = true;
|
||||
|
||||
new bootstrap.Modal(document.getElementById('deleteCompanyModal')).show();
|
||||
}
|
||||
|
||||
function validateHardDelete(value) {
|
||||
document.getElementById('hard-delete-btn').disabled = (value !== 'DELETE');
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user