Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -0,0 +1,269 @@
@using PowderCoating.Core.Enums
@using PowderCoating.Web.Controllers
@using Microsoft.AspNetCore.Html
@model List<UsageRow>
@{
ViewData["Title"] = "Usage & Quota";
string BarColor(int used, int max)
{
if (max <= 0) return "bg-secondary";
double pct = (double)used / max;
if (pct >= 1.0) return "bg-danger";
if (pct >= 0.8) return "bg-warning";
return "bg-success";
}
string LimitDisplay(int max) => max == -1 ? "∞" : max == 0 ? "—" : max.ToString();
int BarWidth(int used, int max)
{
if (max <= 0) return 0;
return Math.Min(100, (int)Math.Round((double)used / max * 100));
}
}
@section Styles {
<style>
/* Dark mode: bg-info text-dark badges — text-dark is unreadable in dark mode */
[data-bs-theme="dark"] .badge.bg-info.text-dark {
color: #fff !important;
}
</style>
}
<div class="container-fluid py-3">
<div class="d-flex align-items-center justify-content-between mb-3">
<h4 class="mb-0">
<i class="bi bi-speedometer2 me-2 text-primary"></i>Usage &amp; Quota
</h4>
</div>
@* Summary cards *@
<div class="row g-3 mb-3">
<div class="col-6 col-md-3">
<div class="card text-center border-0 shadow-sm">
<div class="card-body py-2">
<div class="fs-4 fw-bold">@ViewBag.TotalCount</div>
<div class="small text-muted">Companies</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card text-center border-0 shadow-sm border-danger">
<div class="card-body py-2">
<div class="fs-4 fw-bold text-danger">@ViewBag.AtLimitCount</div>
<div class="small text-muted">At or Over Limit</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card text-center border-0 shadow-sm border-warning">
<div class="card-body py-2">
<div class="fs-4 fw-bold text-warning">@ViewBag.NearLimitCount</div>
<div class="small text-muted">Near Limit (≥80%)</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card text-center border-0 shadow-sm">
<div class="card-body py-2">
<div class="fs-4 fw-bold text-success">@(Model.Count - (int)ViewBag.AtLimitCount - (int)ViewBag.NearLimitCount)</div>
<div class="small text-muted">Healthy</div>
</div>
</div>
</div>
</div>
@* Filters *@
<div class="card shadow-sm mb-3">
<div class="card-body py-2">
<form method="get" class="row g-2 align-items-end">
<div class="col-12 col-md-3">
<label class="form-label small mb-1">Search</label>
<input type="text" name="search" class="form-control form-control-sm" value="@ViewBag.Search" placeholder="Company name..." />
</div>
<div class="col-6 col-md-2">
<label class="form-label small mb-1">Status</label>
<select name="status" class="form-select form-select-sm">
<option value="">All Statuses</option>
@foreach (var s in Enum.GetValues<SubscriptionStatus>())
{
<option value="@s" selected="@(ViewBag.StatusFilter == s.ToString())">@s</option>
}
</select>
</div>
<div class="col-6 col-md-2">
<label class="form-label small mb-1">Plan</label>
<select name="plan" class="form-select form-select-sm">
<option value="">All Plans</option>
@foreach (var p in (IEnumerable<dynamic>)ViewBag.PlanConfigs)
{
<option value="@p.Plan" selected="@(ViewBag.PlanFilter == p.Plan.ToString())">@p.DisplayName</option>
}
</select>
</div>
<div class="col-6 col-md-2">
<label class="form-label small mb-1">Show</label>
<select name="concern" class="form-select form-select-sm">
<option value="" selected="@(ViewBag.ConcernFilter == null)">All Companies</option>
<option value="limit" selected="@(ViewBag.ConcernFilter == "limit")">Near or At Limit</option>
<option value="atlimit" selected="@(ViewBag.ConcernFilter == "atlimit")">At Limit Only</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary btn-sm">Filter</button>
<a href="@Url.Action("Index")" class="btn btn-outline-secondary btn-sm ms-1">Clear</a>
</div>
</form>
</div>
</div>
@* Desktop table *@
<div class="card shadow-sm d-none d-lg-block">
<div class="card-header py-2 small fw-semibold">@Model.Count company(ies)</div>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0 align-middle">
<thead class="table-light">
<tr>
<th>Company</th>
<th>Plan</th>
<th>Status</th>
<th>Users</th>
<th>Active Jobs</th>
<th>Customers</th>
<th>Active Quotes</th>
<th>Catalog</th>
<th></th>
</tr>
</thead>
<tbody>
@if (!Model.Any())
{
<tr><td colspan="9" class="text-center text-muted py-4">No companies found.</td></tr>
}
@foreach (var row in Model)
{
var rowClass = row.IsAtLimit ? "table-danger" : row.IsNearLimit ? "table-warning" : "";
<tr class="@rowClass">
<td class="small fw-semibold">
@row.CompanyName
@if (row.IsComped) { <span class="badge bg-info text-dark ms-1">Comped</span> }
@if (!row.IsActive) { <span class="badge bg-secondary ms-1">Inactive</span> }
</td>
<td class="small">@row.PlanName</td>
<td>
@{
var sc = row.Status switch {
SubscriptionStatus.Active => "success",
SubscriptionStatus.GracePeriod => "warning",
SubscriptionStatus.Expired => "danger",
_ => "secondary"
};
}
<span class="badge bg-@sc">@row.Status</span>
</td>
<td>@UsageCell(row.Users, row.MaxUsers)</td>
<td>@UsageCell(row.ActiveJobs, row.MaxActiveJobs)</td>
<td>@UsageCell(row.Customers, row.MaxCustomers)</td>
<td>@UsageCell(row.ActiveQuotes, row.MaxActiveQuotes)</td>
<td>@UsageCell(row.CatalogItems, row.MaxCatalogItems)</td>
<td>
<a asp-controller="SubscriptionManagement" asp-action="Manage"
asp-route-id="@row.CompanyId" class="btn btn-outline-secondary btn-sm py-0">Manage</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
@* Mobile card view — shown on screens < 992px *@
<div class="mobile-card-view d-lg-none">
<div class="small text-muted mb-2">@Model.Count company(ies)</div>
@if (!Model.Any())
{
<div class="text-center text-muted py-4">No companies found.</div>
}
<div class="mobile-card-list">
@foreach (var row in Model)
{
var alertIcon = row.IsAtLimit ? "bi-exclamation-circle text-danger" : row.IsNearLimit ? "bi-exclamation-triangle text-warning" : "";
var iconBg = row.IsAtLimit ? "bg-danger" : row.IsNearLimit ? "bg-warning" : "bg-primary";
var statusColor = row.Status switch {
SubscriptionStatus.Active => "success",
SubscriptionStatus.GracePeriod => "warning",
SubscriptionStatus.Expired => "danger",
_ => "secondary"
};
<a asp-controller="SubscriptionManagement" asp-action="Manage"
asp-route-id="@row.CompanyId" class="mobile-data-card text-decoration-none">
<div class="mobile-card-header">
<div class="mobile-card-icon @iconBg">
<i class="bi bi-building"></i>
</div>
<div class="mobile-card-title">
<h6>
@row.CompanyName
@if (row.IsComped) { <span class="badge bg-info ms-1">Comped</span> }
@if (!row.IsActive) { <span class="badge bg-secondary ms-1">Inactive</span> }
</h6>
<small>@row.PlanName &mdash; <span class="badge bg-@statusColor">@row.Status</span></small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Users</span>
<span class="mobile-card-value">@row.Users / @LimitDisplay(row.MaxUsers)</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Active Jobs</span>
<span class="mobile-card-value">@row.ActiveJobs / @LimitDisplay(row.MaxActiveJobs)</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Customers</span>
<span class="mobile-card-value">@row.Customers / @LimitDisplay(row.MaxCustomers)</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Active Quotes</span>
<span class="mobile-card-value">@row.ActiveQuotes / @LimitDisplay(row.MaxActiveQuotes)</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Catalog Items</span>
<span class="mobile-card-value">@row.CatalogItems / @LimitDisplay(row.MaxCatalogItems)</span>
</div>
</div>
<div class="mobile-card-footer">
<span class="btn btn-sm btn-outline-primary">Manage →</span>
</div>
</a>
}
</div>
</div>
</div>
@{
IHtmlContent UsageCell(int used, int max)
{
if (max == -1)
{
return new HtmlString($"<span class=\"small\">{used} / ∞</span>");
}
if (max == 0)
{
return new HtmlString($"<span class=\"small text-muted\">{used} / —</span>");
}
var pct = Math.Min(100, (int)Math.Round((double)used / max * 100));
var color = pct >= 100 ? "danger" : pct >= 80 ? "warning" : "success";
return new HtmlString($@"
<div style=""min-width:80px"">
<div class=""d-flex justify-content-between small mb-0"">
<span>{used}</span><span class=""text-muted"">/{max}</span>
</div>
<div class=""progress"" style=""height:4px"">
<div class=""progress-bar bg-{color}"" style=""width:{pct}%""></div>
</div>
</div>");
}
}