Add server-side dismiss persistence and SuperAdmin onboarding progress page

Progress widget dismiss now POSTs to Dashboard/DismissProgressWidget, writing
GuidedActivationDismissedAt to the DB so the widget stays hidden across devices
and cache clears (localStorage alone wasn't enough). BuildShopProgressWidgetAsync
suppresses the widget server-side when AllDone + dismissed.

New SuperAdmin page at /OnboardingProgress shows the activation funnel across
all tenant companies: wizard status, chosen path, milestone progress bar, key
dates (first job/quote, first invoice, workflow completed, widget dismissed),
and a status badge (Not Started / In Progress / Complete / Dismissed). Nav link
added under Users & Activity in the Platform Management sidebar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-29 09:23:20 -04:00
parent 73df72ab97
commit 8de9cd04b8
6 changed files with 311 additions and 1 deletions
@@ -771,7 +771,38 @@ public class DashboardController : Controller
}
};
return new ShopProgressWidgetViewModel { Items = items.Where(i => i != null).Select(i => i!).ToList() };
var vm = new ShopProgressWidgetViewModel { Items = items.Where(i => i != null).Select(i => i!).ToList() };
// Suppress widget if the user already dismissed it after completing all steps
if (vm.AllDone && prefs.GuidedActivationDismissedAt.HasValue)
return null;
return vm;
}
/// <summary>
/// Persists the company admin's dismissal of the progress widget completion state.
/// Sets <c>GuidedActivationDismissedAt</c> so the widget stays hidden across devices
/// and browser sessions (localStorage alone wouldn't survive a cleared cache).
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DismissProgressWidget()
{
var companyId = _tenantContext.GetCurrentCompanyId();
if (companyId == null)
return Json(new { success = false });
var company = await _unitOfWork.Companies.GetByIdAsync(companyId.Value, false, c => c.Preferences!);
if (company?.Preferences == null)
return Json(new { success = false });
company.Preferences.GuidedActivationDismissedAt = DateTime.UtcNow;
await _unitOfWork.CompleteAsync();
_logger.LogInformation("Progress widget dismissed for company {CompanyId}", companyId.Value);
return Json(new { success = true });
}
/// <summary>