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:
@@ -82,19 +82,38 @@ public class CompaniesController : Controller
|
||||
{
|
||||
var ids = companyDtos.Select(c => c.Id).ToList();
|
||||
var summary = await _companyList.GetCountSummaryAsync(ids);
|
||||
var companyById = companies.ToDictionary(c => c.Id);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
foreach (var dto in companyDtos)
|
||||
{
|
||||
dto.JobCount = summary.JobCounts.GetValueOrDefault(dto.Id, 0);
|
||||
dto.QuoteCount = summary.QuoteCounts.GetValueOrDefault(dto.Id, 0);
|
||||
dto.JobCount = summary.JobCounts.GetValueOrDefault(dto.Id, 0);
|
||||
dto.QuoteCount = summary.QuoteCounts.GetValueOrDefault(dto.Id, 0);
|
||||
dto.CustomerCount = summary.CustomerCounts.GetValueOrDefault(dto.Id, 0);
|
||||
|
||||
if (summary.WizardInfo.TryGetValue(dto.Id, out var w))
|
||||
{
|
||||
dto.WizardCompleted = true;
|
||||
dto.WizardCompletedAt = w.CompletedAt;
|
||||
dto.WizardCompleted = true;
|
||||
dto.WizardCompletedAt = w.CompletedAt;
|
||||
dto.WizardCompletedByName = w.CompletedByName;
|
||||
}
|
||||
|
||||
// Health badge
|
||||
var lastLogin = summary.LastLoginDates.TryGetValue(dto.Id, out var ll) ? ll : null;
|
||||
var daysSince = lastLogin.HasValue ? (int)(now - lastLogin.Value).TotalDays : -1;
|
||||
var j30 = summary.Jobs30Counts.GetValueOrDefault(dto.Id, 0);
|
||||
var j90 = summary.Jobs90Counts.GetValueOrDefault(dto.Id, 0);
|
||||
|
||||
if (companyById.TryGetValue(dto.Id, out var co))
|
||||
{
|
||||
var (score, _) = CompanyHealthHelper.ComputeHealth(co, daysSince, j30, j90, dto.JobCount, now);
|
||||
var neverActivated = dto.JobCount == 0 && dto.CustomerCount == 0 && dto.QuoteCount == 0
|
||||
&& dto.CreatedAt < now.AddDays(-7);
|
||||
dto.HealthScore = score;
|
||||
dto.HealthRisk = CompanyHealthHelper.ToRiskLevel(score, neverActivated).ToString();
|
||||
}
|
||||
|
||||
dto.LastLoginDate = lastLogin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +202,8 @@ public class CompaniesController : Controller
|
||||
.GetByIdAsync(id, ignoreQueryFilters: true,
|
||||
c => c.Users,
|
||||
c => c.Customers,
|
||||
c => c.Jobs);
|
||||
c => c.Jobs,
|
||||
c => c.Preferences!);
|
||||
|
||||
if (company == null)
|
||||
{
|
||||
@@ -196,6 +216,51 @@ public class CompaniesController : Controller
|
||||
ViewBag.PlanConfigs = (await _unitOfWork.SubscriptionPlanConfigs.FindAsync(
|
||||
c => c.IsActive, ignoreQueryFilters: true)).OrderBy(c => c.SortOrder).ToList();
|
||||
|
||||
// Health data
|
||||
var summary = await _companyList.GetCountSummaryAsync(new[] { id });
|
||||
var now = DateTime.UtcNow;
|
||||
var lastLogin = summary.LastLoginDates.TryGetValue(id, out var ll) ? ll : null;
|
||||
var daysSince = lastLogin.HasValue ? (int)(now - lastLogin.Value).TotalDays : -1;
|
||||
var j30 = summary.Jobs30Counts.GetValueOrDefault(id, 0);
|
||||
var j90 = summary.Jobs90Counts.GetValueOrDefault(id, 0);
|
||||
var totalJobs = companyDto.JobCount;
|
||||
var totalCust = companyDto.CustomerCount;
|
||||
var totalQuotes = summary.QuoteCounts.GetValueOrDefault(id, 0);
|
||||
|
||||
var (healthScore, healthSignals) = CompanyHealthHelper.ComputeHealth(company, daysSince, j30, j90, totalJobs, now);
|
||||
var neverActivated = totalJobs == 0 && totalCust == 0 && totalQuotes == 0
|
||||
&& company.CreatedAt < now.AddDays(-7);
|
||||
var riskLevel = CompanyHealthHelper.ToRiskLevel(healthScore, neverActivated);
|
||||
|
||||
ViewBag.HealthScore = healthScore;
|
||||
ViewBag.HealthRisk = riskLevel.ToString();
|
||||
ViewBag.HealthSignals = healthSignals;
|
||||
ViewBag.Jobs30 = j30;
|
||||
ViewBag.Jobs90 = j90;
|
||||
ViewBag.LastLoginDate = lastLogin;
|
||||
|
||||
// Onboarding data (from Preferences)
|
||||
var prefs = company.Preferences;
|
||||
int steps = 0;
|
||||
if (prefs?.FirstJobCreatedAt.HasValue == true || prefs?.FirstQuoteCreatedAt.HasValue == true) steps++;
|
||||
if (prefs?.FirstInvoiceCreatedAt.HasValue == true) steps++;
|
||||
if (prefs?.FirstWorkflowCompletedAt.HasValue == true) steps++;
|
||||
|
||||
ViewBag.Onboarding = new PowderCoating.Web.ViewModels.Platform.OnboardingProgressRowViewModel
|
||||
{
|
||||
CompanyId = company.Id,
|
||||
CompanyName = company.CompanyName ?? "",
|
||||
WizardCompleted = prefs?.SetupWizardCompleted ?? false,
|
||||
OnboardingPath = prefs?.OnboardingPath,
|
||||
StepsCompleted = steps,
|
||||
TotalSteps = 3,
|
||||
FirstJobCreatedAt = prefs?.FirstJobCreatedAt,
|
||||
FirstQuoteCreatedAt = prefs?.FirstQuoteCreatedAt,
|
||||
FirstInvoiceCreatedAt = prefs?.FirstInvoiceCreatedAt,
|
||||
FirstWorkflowCompletedAt = prefs?.FirstWorkflowCompletedAt,
|
||||
GuidedActivationDismissedAt = prefs?.GuidedActivationDismissedAt,
|
||||
};
|
||||
|
||||
return View(companyDto);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
Reference in New Issue
Block a user