Consolidate company admin screens: health badge on list, tabbed detail page
Companies/Index: - Added Health badge column (Healthy / At Risk / Critical / Never Active) with the numeric score in a tooltip; computed from the same signals as CompanyHealth/Index using the new shared CompanyHealthHelper Companies/Details: - Converted flat card layout to five tabs: Overview, Users, Subscription, Onboarding, Health; URL hash is preserved so the active tab survives page refresh and back navigation - Subscription tab shows plan/status/dates with an expiry countdown and a "Manage Subscription & Features" button to the full Manage page - Onboarding tab shows wizard completion, milestone progress bar, and first-activity dates (previously only on the standalone page) - Health tab shows score gauge, risk badge, and individual risk signals with a link through to the full CompanyHealth dashboard - JS moved to wwwroot/js/companies-details.js (avoids inline-script failures) Infrastructure: - Extracted ComputeHealth / ToRiskLevel / ChurnRisk to CompanyHealthHelper.cs (same Controllers namespace); CompanyHealthController delegates to it - CompanyCountSummary extended with Jobs30Counts, Jobs90Counts, LastLoginDates (3 extra GROUP BY queries scoped to the current page IDs, not all companies) - CompanyListDto gains HealthScore, HealthRisk, LastLoginDate Navigation: - Removed "Onboarding Progress" hub card from People & Activity; the data is now surfaced directly on the Companies/Details Onboarding tab Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -118,15 +118,12 @@ public class CompanyHealthController : Controller
|
||||
var tquotes = totalQuotes.TryGetValue(c.Id, out var tq) ? tq : 0;
|
||||
var planName = planNames.TryGetValue(c.SubscriptionPlan, out var pn) ? pn : c.SubscriptionPlan.ToString();
|
||||
|
||||
var (score, signals) = ComputeHealth(c, daysSince, j30v, j90v, tjobs, now);
|
||||
var (score, signals) = CompanyHealthHelper.ComputeHealth(c, daysSince, j30v, j90v, tjobs, now);
|
||||
|
||||
var neverActivated = tjobs == 0 && tcust == 0 && tquotes == 0
|
||||
&& c.CreatedAt < now.AddDays(-7);
|
||||
|
||||
var riskLevel = neverActivated ? ChurnRisk.NeverActivated
|
||||
: score >= 75 ? ChurnRisk.Healthy
|
||||
: score >= 45 ? ChurnRisk.AtRisk
|
||||
: ChurnRisk.Critical;
|
||||
var riskLevel = CompanyHealthHelper.ToRiskLevel(score, neverActivated);
|
||||
|
||||
var configHealth = configHealthMap.TryGetValue(c.Id, out var ch)
|
||||
? ch : new CompanyConfigHealth { CompanyId = c.Id };
|
||||
@@ -187,112 +184,10 @@ public class CompanyHealthController : Controller
|
||||
return View(all);
|
||||
}
|
||||
|
||||
// ── Health score algorithm ──────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Computes a 0–100 health score and a list of human-readable risk signals for a
|
||||
/// single company based on its subscription status, login recency, and job activity.
|
||||
/// <para>
|
||||
/// Scoring rules (penalties are cumulative, floor is 0):
|
||||
/// <list type="bullet">
|
||||
/// <item>Disabled account: score immediately set to 0, no further evaluation.</item>
|
||||
/// <item>Subscription expired past the grace period: −50 pts.</item>
|
||||
/// <item>Subscription within grace period: −30 pts.</item>
|
||||
/// <item>Subscription expiring within 7 days: −20 pts; within 14 days: −10 pts.</item>
|
||||
/// <item>Comped companies skip subscription checks entirely.</item>
|
||||
/// <item>Never logged in: −30 pts; no login in 90+ days: −30; 60+d: −20; 30+d: −10.</item>
|
||||
/// <item>No jobs ever: −20 pts; no jobs in last 90 days: −10; no jobs in 30d: −5.</item>
|
||||
/// </list>
|
||||
/// A <c>daysSinceLogin</c> value of −1 means "never logged in" and is distinct
|
||||
/// from "logged in exactly 0 days ago" (i.e. today).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private static (int score, List<string> signals) ComputeHealth(
|
||||
PowderCoating.Core.Entities.Company c, int daysSinceLogin,
|
||||
int j30, int j90, int totalJobs, DateTime now)
|
||||
{
|
||||
var score = 100;
|
||||
var signals = new List<string>();
|
||||
|
||||
if (!c.IsActive)
|
||||
{
|
||||
signals.Add("Account disabled");
|
||||
return (0, signals);
|
||||
}
|
||||
|
||||
// Subscription health (skip for comped)
|
||||
if (!c.IsComped && c.SubscriptionEndDate.HasValue)
|
||||
{
|
||||
var daysUntil = (int)(c.SubscriptionEndDate.Value.Date - now.Date).TotalDays;
|
||||
if (daysUntil < -AppConstants.SubscriptionConstants.GracePeriodDays)
|
||||
{
|
||||
score -= 50;
|
||||
signals.Add("Subscription expired");
|
||||
}
|
||||
else if (daysUntil < 0)
|
||||
{
|
||||
score -= 30;
|
||||
signals.Add("In grace period");
|
||||
}
|
||||
else if (daysUntil <= 7)
|
||||
{
|
||||
score -= 20;
|
||||
signals.Add($"Expires in {daysUntil}d");
|
||||
}
|
||||
else if (daysUntil <= 14)
|
||||
{
|
||||
score -= 10;
|
||||
signals.Add($"Expires in {daysUntil}d");
|
||||
}
|
||||
}
|
||||
|
||||
// Login activity
|
||||
if (daysSinceLogin == -1)
|
||||
{
|
||||
score -= 30;
|
||||
signals.Add("Never logged in");
|
||||
}
|
||||
else if (daysSinceLogin >= 90)
|
||||
{
|
||||
score -= 30;
|
||||
signals.Add($"No login {daysSinceLogin}d");
|
||||
}
|
||||
else if (daysSinceLogin >= 60)
|
||||
{
|
||||
score -= 20;
|
||||
signals.Add($"No login {daysSinceLogin}d");
|
||||
}
|
||||
else if (daysSinceLogin >= 30)
|
||||
{
|
||||
score -= 10;
|
||||
signals.Add($"No login {daysSinceLogin}d");
|
||||
}
|
||||
|
||||
// Job activity
|
||||
if (totalJobs == 0)
|
||||
{
|
||||
score -= 20;
|
||||
signals.Add("No jobs ever");
|
||||
}
|
||||
else if (j90 == 0)
|
||||
{
|
||||
score -= 10;
|
||||
signals.Add("No jobs in 90d");
|
||||
}
|
||||
else if (j30 == 0)
|
||||
{
|
||||
score -= 5;
|
||||
signals.Add("No jobs in 30d");
|
||||
}
|
||||
|
||||
return (Math.Max(0, score), signals);
|
||||
}
|
||||
}
|
||||
|
||||
// ── View models ────────────────────────────────────────────────────────────────
|
||||
|
||||
public enum ChurnRisk { Healthy, AtRisk, Critical, NeverActivated }
|
||||
|
||||
public class CompanyHealthDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
Reference in New Issue
Block a user