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:
@@ -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>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Core.Interfaces;
|
||||
using PowderCoating.Shared.Constants;
|
||||
using PowderCoating.Web.ViewModels.Platform;
|
||||
|
||||
namespace PowderCoating.Web.Controllers;
|
||||
|
||||
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
|
||||
public class OnboardingProgressController : Controller
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public OnboardingProgressController(IUnitOfWork unitOfWork, UserManager<ApplicationUser> userManager)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows onboarding and activation status for every tenant company. SuperAdmin-only.
|
||||
/// Reads <c>CompanyPreferences</c> fields written by the setup wizard, guided activation,
|
||||
/// and the jobs/quotes/invoices controllers to give a cross-tenant activation funnel view.
|
||||
/// </summary>
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var companies = (await _unitOfWork.Companies.GetAllAsync(
|
||||
ignoreQueryFilters: true,
|
||||
c => c.Preferences!))
|
||||
.Where(c => !c.IsDeleted)
|
||||
.OrderBy(c => c.CompanyName)
|
||||
.ToList();
|
||||
|
||||
var rows = companies.Select(c => BuildRow(c)).ToList();
|
||||
|
||||
return View(new OnboardingProgressIndexViewModel { Rows = rows });
|
||||
}
|
||||
|
||||
private static OnboardingProgressRowViewModel BuildRow(Company company)
|
||||
{
|
||||
var prefs = company.Preferences;
|
||||
var wizardDone = prefs?.SetupWizardCompleted ?? false;
|
||||
|
||||
// Mirror the same 6-step logic used in DashboardController.BuildShopProgressWidgetAsync
|
||||
// Steps: first job/quote, status history (unknown here — omit), first invoice,
|
||||
// team size (unknown here — omit), customized lookups (unknown — omit), payment defaults.
|
||||
// We track the 3 date-stamped milestones we can derive from prefs alone.
|
||||
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++;
|
||||
const int total = 3;
|
||||
|
||||
OnboardingStatus status;
|
||||
if (!wizardDone)
|
||||
status = OnboardingStatus.NotStarted;
|
||||
else if (prefs?.FirstWorkflowCompletedAt.HasValue == true && prefs.GuidedActivationDismissedAt.HasValue)
|
||||
status = OnboardingStatus.Dismissed;
|
||||
else if (prefs?.FirstWorkflowCompletedAt.HasValue == true)
|
||||
status = OnboardingStatus.Complete;
|
||||
else if (steps > 0)
|
||||
status = OnboardingStatus.InProgress;
|
||||
else
|
||||
status = OnboardingStatus.NotStarted;
|
||||
|
||||
return new OnboardingProgressRowViewModel
|
||||
{
|
||||
CompanyId = company.Id,
|
||||
CompanyName = company.CompanyName ?? "Unknown",
|
||||
WizardCompleted = wizardDone,
|
||||
OnboardingPath = prefs?.OnboardingPath,
|
||||
StepsCompleted = steps,
|
||||
TotalSteps = total,
|
||||
FirstJobCreatedAt = prefs?.FirstJobCreatedAt,
|
||||
FirstQuoteCreatedAt = prefs?.FirstQuoteCreatedAt,
|
||||
FirstInvoiceCreatedAt = prefs?.FirstInvoiceCreatedAt,
|
||||
FirstWorkflowCompletedAt = prefs?.FirstWorkflowCompletedAt,
|
||||
GuidedActivationDismissedAt = prefs?.GuidedActivationDismissedAt,
|
||||
Status = status
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user