Onboarding overhaul: slim wizard, progress widget, guided activation UX

Setup Wizard: reduced from 10 steps to 5 (Company Info → QB Migration →
Pricing Defaults → Named Ovens → Notifications). Removed Doc Numbering,
Job Settings, Payment Terms, Pricing Tiers, and Team Members steps — these
all have sensible defaults and are accessible any time in Company Settings.
Wizard now completes in ~5 minutes instead of 15–20.

Dashboard progress widget (new): "Get the most out of your shop" checklist
appears for Company Admins after wizard completion. Tracks six post-setup
activation tasks with dynamic progress badge, motivating subtitle copy,
collapsed-state persistence via localStorage, and a full completion state
("Your shop is fully set up 🎉") that replaces the checklist at 100%.
The next recommended step is highlighted with a solid CTA button and a
subtle blue row tint. Completed steps show encouraging green subtext instead
of just "Done". Widget disappears from controller when AllDone would have
caused a silent vanish — now renders the completion state instead.

Guided activation (Daily Board): rewrote the BoardIntroStep callout to lead
with "This is your shop in real time" and a plain-English description of the
board's purpose. Added a separate InstructionText field to
GuidedActivationCalloutViewModel so the "Move this job to the next stage"
action prompt renders as a distinct bold line with an arrow icon rather than
being buried in the body copy. After the stage change, the confirmation
callout now reads "Nice — your workflow just updated" to reinforce what just
happened before prompting the invoice step.

All copy passes the "shop owner, not SaaS" test: no technical jargon,
benefit-driven descriptions, natural language throughout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 21:10:47 -04:00
parent 4d27a378ac
commit 8aae30765f
30 changed files with 10870 additions and 333 deletions
+104 -5
View File
@@ -1,10 +1,17 @@
@using PowderCoating.Shared.Constants
@using PowderCoating.Web.Controllers
@using PowderCoating.Web.ViewModels.GuidedActivation
@model List<JobBoardColumn>
@{
ViewData["Title"] = "Jobs Board";
bool showTerminal = ViewBag.ShowTerminal == true;
int totalTerminal = (int)(ViewBag.TotalTerminal ?? 0);
var guidedActivationCallout = ViewBag.GuidedActivationCallout as GuidedActivationCalloutViewModel;
string? guidedActivation = ViewBag.GuidedActivation as string;
int? highlightJobId = ViewBag.GuidedActivationHighlightJobId is int highlightedId ? highlightedId : null;
var highlightedCard = highlightJobId.HasValue
? Model.SelectMany(c => c.Jobs).FirstOrDefault(j => j.Id == highlightJobId.Value)
: null;
}
@section Styles {
@@ -108,7 +115,19 @@
}
.board-card:hover { background: var(--pcl-paper-2); color: var(--pcl-ink); }
.board-card.board-card-hot { box-shadow: inset 2px 0 0 var(--pcl-bad); }
.board-card.board-card-guided {
border-color: var(--pcl-cool);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--pcl-cool) 24%, transparent);
background: color-mix(in srgb, var(--pcl-cool) 8%, var(--pcl-card));
}
.board-card.dragging { opacity: .5; cursor: grabbing; }
.board-guided-badge {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--pcl-cool);
}
/* Card content */
.card-job-number { font-family: var(--font-mono); font-weight: 500; font-size: .8rem; color: var(--pcl-ink); }
@@ -171,6 +190,40 @@
}
<div class="board-outer">
@if (guidedActivationCallout?.Show == true)
{
<div class="alert alert-info alert-permanent border-0 shadow-sm mb-3">
<div class="d-flex flex-column flex-xl-row gap-3 align-items-xl-center justify-content-between">
<div>
<div class="fw-semibold mb-1">@guidedActivationCallout.Title</div>
<div>@guidedActivationCallout.Message</div>
@if (!string.IsNullOrWhiteSpace(guidedActivationCallout.InstructionText))
{
<div class="fw-semibold mt-2 small" style="color:var(--pcl-ink);">
<i class="bi bi-arrow-right-circle me-1"></i>@guidedActivationCallout.InstructionText
</div>
}
</div>
<div class="d-flex gap-2 flex-wrap">
@if (!string.IsNullOrWhiteSpace(guidedActivationCallout.ActionText))
{
<a href="@Url.Action(guidedActivationCallout.ActionName, guidedActivationCallout.ActionController, guidedActivationCallout.ActionRouteValues)"
class="btn btn-primary">
@guidedActivationCallout.ActionText
</a>
}
@if (!string.IsNullOrWhiteSpace(guidedActivationCallout.SecondaryActionText))
{
<a href="@Url.Action(guidedActivationCallout.SecondaryActionName, guidedActivationCallout.SecondaryActionController, guidedActivationCallout.SecondaryActionRouteValues)"
class="btn btn-outline-primary">
@guidedActivationCallout.SecondaryActionText
</a>
}
</div>
</div>
</div>
}
@* Toolbar *@
@{
var _totalOnFloor = Model.Sum(c => c.Jobs.Count);
@@ -181,7 +234,11 @@
@* Left: view switch + live stats *@
<div class="d-flex align-items-center gap-3">
<div class="board-view-switch">
<a asp-action="Board" class="active">Board</a>
<a asp-action="Board"
asp-route-showTerminal="@showTerminal"
asp-route-guidedActivation="@guidedActivation"
asp-route-highlightJobId="@highlightJobId"
class="active">Board</a>
<a asp-action="Index">List</a>
</div>
<span class="mono" style="font-size:.75rem;color:var(--pcl-steel)">
@@ -195,7 +252,7 @@
@* Right: actions *@
<div class="d-flex align-items-center gap-2">
<a href="@Url.Action("Board", new { showTerminal = !showTerminal })"
<a href="@Url.Action("Board", new { showTerminal = !showTerminal, guidedActivation, highlightJobId })"
class="btn btn-sm btn-outline-secondary"
id="toggleTerminalBtn">
<i class="bi bi-archive me-1"></i>
@@ -286,12 +343,16 @@
_ => "board-priority-secondary"
};
<a href="@Url.Action("Details", new { id = card.Id })"
class="board-card@(card.IsOverdue ? " board-card-hot" : "")"
class="board-card@(card.IsOverdue ? " board-card-hot" : "")@(highlightJobId == card.Id ? " board-card-guided" : "")"
data-job-id="@card.Id"
onclick="return false">
<div class="d-flex align-items-start justify-content-between gap-1">
<span class="card-job-number">@card.JobNumber</span>
@if (highlightJobId == card.Id)
{
<span class="board-guided-badge">Guided Job</span>
}
</div>
<div class="card-customer">@card.CustomerName</div>
@@ -347,6 +408,8 @@
<script>
(function () {
const COMPANY_ID = '@(User.FindFirst("CompanyId")?.Value ?? "0")';
const guidedActivation = '@(guidedActivation ?? string.Empty)';
const highlightJobId = @(highlightJobId?.ToString() ?? "null");
// ── Show Completed persistence ───────────────────────────────────────────
const TERMINAL_KEY = `jobBoard_showTerminal_${COMPANY_ID}`;
@@ -402,6 +465,28 @@
}
}
function ensureGuidedCardVisible() {
if (!highlightJobId) return;
const card = document.querySelector(`.board-card[data-job-id="${highlightJobId}"]`);
if (!card) return;
const column = card.closest('.board-column');
if (column?.classList.contains('col-hidden')) {
const statusId = parseInt(column.dataset.statusId);
const hidden = loadHiddenCols();
const idx = hidden.indexOf(statusId);
if (idx > -1) {
hidden.splice(idx, 1);
saveHiddenCols(hidden);
applyVisibility();
}
}
card.classList.add('board-card-guided');
card.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
}
document.querySelectorAll('.col-vis-check').forEach(cb => {
cb.addEventListener('change', () => {
const id = parseInt(cb.dataset.statusId);
@@ -458,6 +543,7 @@
});
applyColOrder();
ensureGuidedCardVisible();
// ── Drag & drop + card navigation ────────────────────────────────────────
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value
@@ -506,13 +592,26 @@
'Content-Type': 'application/json',
'RequestVerificationToken': token
},
body: JSON.stringify({ jobId, newStatusId: newStatus })
body: JSON.stringify({
jobId,
newStatusId: newStatus,
guidedActivation,
highlightJobId
})
})
.then(r => r.json())
.then(data => {
if (data.success) {
// Update card's priority border stays — status shown by column
showToast(`Moved to ${data.newStatusDisplay}`, true);
if (data.guidedActivationNext && highlightJobId && jobId === highlightJobId) {
const nextUrl = new URL(window.location.href);
nextUrl.searchParams.set('guidedActivation', data.guidedActivationNext);
nextUrl.searchParams.set('highlightJobId', String(highlightJobId));
setTimeout(() => { window.location.href = nextUrl.toString(); }, 700);
return;
}
} else {
// Revert
oldColEl.insertBefore(card, oldColEl.children[evt.oldIndex] ?? null);
@@ -27,12 +27,21 @@
<form asp-action="Create" method="post" id="jobCreateForm">
@Html.AntiForgeryToken()
<input type="hidden" name="guidedActivation" value="@ViewBag.GuidedActivation" />
@if (ViewBag.TemplateId != null)
{
<input type="hidden" name="SourceTemplateId" value="@ViewBag.TemplateId">
}
<partial name="_ValidationSummary" />
@if ((ViewBag.GuidedActivation as string) == PowderCoating.Shared.Constants.AppConstants.GuidedActivation.JobFirstPath)
{
<div class="alert alert-primary alert-permanent border-0 shadow-sm mb-4">
<div class="fw-semibold mb-1">Step 1: Create your first sample job</div>
<div>We've prefilled a quick example. You can edit anything before saving.</div>
</div>
}
<!-- Job Details Card -->
<div class="card mb-4">
<div class="card-header">
@@ -3,6 +3,7 @@
@{
ViewData["Title"] = $"Job {Model.JobNumber}";
ViewData["PageIcon"] = "bi-briefcase";
var guidedActivationCallout = ViewBag.GuidedActivationCallout as PowderCoating.Web.ViewModels.GuidedActivation.GuidedActivationCalloutViewModel;
}
<div class="row justify-content-center">
@@ -38,6 +39,34 @@
</div>
</div>
@if (guidedActivationCallout?.Show == true)
{
<div class="alert alert-primary alert-permanent border-0 shadow-sm mb-4">
<div class="d-flex flex-column flex-lg-row gap-3 align-items-lg-center justify-content-between">
<div>
<div class="fw-semibold mb-1">@guidedActivationCallout.Title</div>
<div>@guidedActivationCallout.Message</div>
</div>
<div class="d-flex gap-2 flex-wrap">
@if (!string.IsNullOrWhiteSpace(guidedActivationCallout.ActionText))
{
<a href="@Url.Action(guidedActivationCallout.ActionName, guidedActivationCallout.ActionController, guidedActivationCallout.ActionRouteValues)"
class="btn btn-primary">
@guidedActivationCallout.ActionText
</a>
}
@if (!string.IsNullOrWhiteSpace(guidedActivationCallout.SecondaryActionText))
{
<a href="@Url.Action(guidedActivationCallout.SecondaryActionName, guidedActivationCallout.SecondaryActionController, guidedActivationCallout.SecondaryActionRouteValues)"
class="btn btn-outline-primary">
@guidedActivationCallout.SecondaryActionText
</a>
}
</div>
</div>
</div>
}
<div class="row g-4">
<!-- Left Column -->
<div class="col-lg-8">