Initial commit
This commit is contained in:
@@ -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 & 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 — <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>");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user