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
@@ -1,12 +1,15 @@
@model PowderCoating.Application.DTOs.Dashboard.DashboardViewModel
@using Microsoft.AspNetCore.Html
@using PowderCoating.Application.DTOs.Health
@using PowderCoating.Web.ViewModels.Dashboard
@{
ViewData["Title"] = "Dashboard";
var today = DateTime.Today;
var currentMonth = DateTime.Now.ToString("MMMM yyyy");
var configHealth = ViewBag.ConfigHealth as CompanyConfigHealth;
var guidedActivationBanner = ViewBag.GuidedActivationBanner as PowderCoating.Web.ViewModels.GuidedActivation.GuidedActivationBannerViewModel;
var shopProgressWidget = ViewBag.ShopProgressWidget as ShopProgressWidgetViewModel;
}
<!-- Hero Brief -->
@@ -56,6 +59,34 @@
</div>
</div>
@if (guidedActivationBanner?.Show == true)
{
<div class="row mb-4">
<div class="col-12">
<div class="card border-0 shadow-sm @(guidedActivationBanner.IsDismissed ? "" : "border-start border-4 border-primary")">
<div class="card-body py-3">
<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">@guidedActivationBanner.Title</div>
<div class="text-muted">@guidedActivationBanner.Message</div>
</div>
<div>
<a asp-controller="GuidedActivation" asp-action="Start" class="btn @(guidedActivationBanner.IsDismissed ? "btn-outline-primary" : "btn-primary")">
@guidedActivationBanner.ActionText
</a>
</div>
</div>
</div>
</div>
</div>
</div>
}
@if (shopProgressWidget != null)
{
@await Html.PartialAsync("_ShopProgressWidget", shopProgressWidget)
}
@* Config health alert — only shown when there are setup gaps *@
@if (configHealth != null && !configHealth.IsHealthy)
{
@@ -777,6 +808,7 @@
</div>
@section Scripts {
<script src="~/js/shop-progress-widget.js" asp-append-version="true"></script>
<script>
// Powder Orders - Mark as Ordered
document.querySelectorAll('.mark-ordered-btn').forEach(btn => {
@@ -1250,4 +1282,3 @@
};
}
}
@@ -0,0 +1,110 @@
@using PowderCoating.Web.ViewModels.Dashboard
@model ShopProgressWidgetViewModel
<div class="card border-0 shadow-sm mb-4" id="shopProgressWidget">
<div class="card-header d-flex align-items-center gap-2 py-2 px-4"
style="background:var(--pcl-paper-2);border-bottom:1px solid var(--pcl-rule);">
<i class="bi bi-rocket-takeoff" style="color:var(--pcl-blue);"></i>
<span class="fw-semibold" style="color:var(--pcl-ink);">Get the most out of your shop</span>
@if (!Model.AllDone)
{
<span class="ms-auto badge rounded-pill bg-secondary">@Model.BadgeText</span>
}
<button class="btn btn-link btn-sm p-0 @(Model.AllDone ? "ms-auto" : "ms-2") text-secondary"
id="shopProgressToggle" title="Collapse" style="line-height:1;">
<i class="bi bi-chevron-up" id="shopProgressChevron"></i>
</button>
</div>
<div id="shopProgressBody">
@if (Model.AllDone)
{
<div class="px-4 py-4 text-center">
<div class="fw-semibold mb-1" style="font-size:1rem;color:var(--pcl-ink);">Your shop is fully set up 🎉</div>
<div class="text-muted mb-3" style="font-size:0.85rem;">You're ready to run everything from here.</div>
<a href="@Url.Action("Create", "Jobs")" class="btn btn-primary btn-sm">
Create job <i class="bi bi-arrow-right ms-1"></i>
</a>
</div>
}
else
{
<div class="px-4 pt-3 pb-1">
<p class="text-muted mb-2" style="font-size:0.85rem;">@Model.SubtitleText</p>
<div class="progress mb-1" style="height:5px;border-radius:3px;">
<div class="progress-bar @(Model.ProgressPercent >= 60 ? "bg-success" : "bg-primary")"
role="progressbar"
style="width:@Model.ProgressPercent%;transition:width 0.4s ease;"
aria-valuenow="@Model.ProgressPercent" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
<ul class="list-group list-group-flush mb-1">
@{
var nextFound = false;
bool? prevDone = null;
}
@foreach (var item in Model.Items)
{
var isNext = !item.Done && !nextFound;
if (isNext) { nextFound = true; }
@if (prevDone.HasValue && prevDone.Value && !item.Done)
{
<li class="list-group-item border-0 px-4 py-0" style="background:transparent;">
<hr class="my-0" style="border-color:var(--pcl-rule);">
</li>
}
prevDone = item.Done;
<li class="list-group-item border-0 d-flex align-items-center gap-3 px-4 py-2"
style="background:@(isNext ? "rgba(13,110,253,0.04)" : "transparent");">
@if (item.Done)
{
<i class="bi bi-check-circle-fill flex-shrink-0"
style="color:var(--pcl-good);font-size:1.1rem;"></i>
}
else
{
<i class="bi @item.Icon flex-shrink-0 text-muted" style="font-size:1.1rem;"></i>
}
<div class="flex-grow-1 min-width-0">
<div class="fw-medium @(item.Done ? "text-muted" : "")" style="font-size:0.875rem;">
@item.Label
@if (isNext)
{
<span class="badge bg-primary bg-opacity-10 text-primary rounded-pill ms-1" style="font-size:0.65rem;">Next</span>
}
</div>
@if (item.Done)
{
@if (!string.IsNullOrEmpty(item.DoneSubLabel))
{
<div style="font-size:0.78rem;color:var(--pcl-good);">@item.DoneSubLabel</div>
}
}
else
{
<div class="text-muted" style="font-size:0.78rem;">@item.SubLabel</div>
}
</div>
@if (item.Done)
{
<span class="flex-shrink-0 fw-medium"
style="font-size:0.78rem;color:var(--pcl-good);">Done</span>
}
else
{
<a href="@item.CtaUrl"
class="btn btn-sm @(isNext ? "btn-primary" : "btn-outline-primary") flex-shrink-0">
@item.CtaText <i class="bi bi-arrow-right ms-1"></i>
</a>
}
</li>
}
</ul>
}
</div>
</div>
@@ -0,0 +1,113 @@
@model PowderCoating.Web.ViewModels.GuidedActivation.GuidedActivationSelectionViewModel
@using PowderCoating.Shared.Constants
@{
ViewData["Title"] = "Start Your First Workflow";
}
@section Styles {
<style>
.ga-shell {
min-height: calc(100vh - 12rem);
display: flex;
align-items: center;
justify-content: center;
}
.ga-card {
width: min(920px, 100%);
border: 0;
border-radius: 1.25rem;
overflow: hidden;
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.14);
}
.ga-hero {
background: linear-gradient(145deg, #0f172a 0%, #1d4ed8 100%);
color: white;
padding: 2.5rem;
}
.ga-option {
border: 1px solid var(--bs-border-color);
border-radius: 1rem;
padding: 1.25rem;
height: 100%;
cursor: pointer;
transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
.ga-option:hover {
border-color: #2563eb;
transform: translateY(-2px);
box-shadow: 0 12px 24px rgba(37, 99, 235, 0.10);
}
.ga-option input {
margin-top: 0.2rem;
}
</style>
}
<div class="ga-shell">
<div class="card ga-card">
<div class="ga-hero">
<div class="text-uppercase small fw-semibold mb-2" style="letter-spacing:0.12em;opacity:0.8;">Guided Activation</div>
<h1 class="h2 fw-bold mb-2">Your shop is set up. Let's run your first workflow.</h1>
<p class="mb-0" style="max-width:42rem;color:rgba(255,255,255,0.82);">
Choose how jobs usually start for your shop and we'll guide you through it with real quotes, jobs, and invoices.
</p>
</div>
<div class="card-body p-4 p-lg-5">
<form asp-action="Select" method="post">
@Html.AntiForgeryToken()
<div asp-validation-summary="ModelOnly" class="text-danger mb-3"></div>
<p class="text-muted fw-semibold mb-3">How do jobs usually start for your shop?</p>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="ga-option d-flex gap-3" for="pathQuoteFirst">
<input asp-for="OnboardingPath" id="pathQuoteFirst" type="radio"
value="@AppConstants.GuidedActivation.QuoteFirstPath" class="form-check-input" />
<span>
<span class="d-block fw-bold fs-5 text-dark">I send a quote first</span>
<span class="d-block text-muted mt-2">
Create a quote, convert it to a job, then invoice when work is complete.
</span>
</span>
</label>
</div>
<div class="col-md-6">
<label class="ga-option d-flex gap-3" for="pathJobFirst">
<input asp-for="OnboardingPath" id="pathJobFirst" type="radio"
value="@AppConstants.GuidedActivation.JobFirstPath" class="form-check-input" />
<span>
<span class="d-block fw-bold fs-5 text-dark">I start with a job</span>
<span class="d-block text-muted mt-2">
For walk-ins or approved work where you start immediately.
</span>
</span>
</label>
</div>
</div>
<div class="d-flex flex-wrap gap-2">
<button type="submit" class="btn btn-primary btn-lg px-4">
Continue
</button>
</div>
</form>
<form asp-action="Skip" method="post" class="mt-3">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-link text-muted px-0">Skip for now</button>
</form>
</div>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
@@ -37,6 +37,7 @@
<form asp-action="Create" method="post" id="invoiceForm">
@Html.AntiForgeryToken()
<input type="hidden" name="guidedActivation" value="@ViewBag.GuidedActivation" />
@if (!ViewData.ModelState.IsValid)
{
@@ -47,6 +48,14 @@
</div>
}
@if (ViewBag.GuidedActivation != null)
{
<div class="alert alert-primary alert-permanent border-0 shadow-sm mb-4">
<div class="fw-semibold mb-1">Optional next step: Create the invoice</div>
<div>This uses the real invoice flow. Review the line items, then save when you want to close the loop with billing.</div>
</div>
}
<input type="hidden" asp-for="PreparedById" />
<input type="hidden" asp-for="JobId" />
<input type="hidden" asp-for="CustomerId" id="hiddenCustomerId" />
@@ -21,6 +21,7 @@
&& (Model.PaymentLinkExpiresAt == null || Model.PaymentLinkExpiresAt <= DateTime.UtcNow);
var onlinePaymentsEnabled = ViewBag.OnlinePaymentsEnabled == true;
var showOnlinePaymentCard = !isDraft && !isVoided && Model.BalanceDue > 0 && onlinePaymentsEnabled;
var guidedActivationCallout = ViewBag.GuidedActivationCallout as PowderCoating.Web.ViewModels.GuidedActivation.GuidedActivationCalloutViewModel;
}
<div class="row justify-content-center">
@@ -69,6 +70,23 @@
</div>
}
@if (guidedActivationCallout?.Show == true)
{
<div class="alert alert-success 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>
<a asp-controller="Dashboard" asp-action="Index" class="btn btn-success">
@guidedActivationCallout.ActionText
</a>
</div>
</div>
</div>
}
<!-- Status Banner -->
<div class="alert alert-@statusColor alert-permanent d-flex align-items-center mb-4">
<i class="bi bi-info-circle me-2" style="font-size:1.4rem;"></i>
+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">
@@ -18,8 +18,17 @@
<form asp-action="Create" asp-controller="Quotes" method="post" id="quoteForm">
@Html.AntiForgeryToken()
<input type="hidden" name="guidedActivation" value="@ViewBag.GuidedActivation" />
<input type="hidden" asp-for="TaxPercent" />
@if ((ViewBag.GuidedActivation as string) == PowderCoating.Shared.Constants.AppConstants.GuidedActivation.QuoteFirstPath)
{
<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 quote</div>
<div>We've prefilled a quick example. You can edit anything before saving.</div>
</div>
}
<!-- Mode Toggle -->
<div class="d-flex align-items-center gap-2 mb-3">
<div class="quote-mode-toggle" role="group" aria-label="Quote mode">
@@ -3,6 +3,8 @@
@{
ViewData["Title"] = $"Quote {Model.QuoteNumber}";
ViewData["PageIcon"] = "bi-file-text";
var guidedActivationCallout = ViewBag.GuidedActivationCallout as PowderCoating.Web.ViewModels.GuidedActivation.GuidedActivationCalloutViewModel;
var guidedActivationMode = ViewBag.GuidedActivationMode as string;
}
<div class="container-fluid mt-4">
@@ -43,7 +45,28 @@
<i class="bi bi-arrow-left me-1"></i>Back
</a>
</div>
</div> <div class="row">
</div>
@if (guidedActivationCallout?.Show == true)
{
<div class="alert alert-primary alert-permanent border-0 shadow-sm d-flex flex-column flex-lg-row gap-3 align-items-lg-center justify-content-between mb-4">
<div>
<div class="fw-semibold mb-1">@guidedActivationCallout.Title</div>
<div>@guidedActivationCallout.Message</div>
</div>
<div>
<form asp-action="ConvertToJob" asp-route-id="@Model.Id" method="post" class="d-inline">
@Html.AntiForgeryToken()
<input type="hidden" name="guidedActivation" value="@guidedActivationMode" />
<button type="submit" class="btn btn-primary">
@guidedActivationCallout.ActionText
</button>
</form>
</div>
</div>
}
<div class="row">
<!-- Left Column: Quote Information -->
<div class="col-lg-8">
<!-- Customer/Prospect Info -->
@@ -1461,6 +1484,7 @@
{
<form asp-action="ConvertToJob" asp-route-id="@Model.Id" method="post" class="d-inline" id="createJobForm">
@Html.AntiForgeryToken()
<input type="hidden" name="guidedActivation" value="@guidedActivationMode" />
<button type="button" class="btn btn-success w-100" data-bs-toggle="modal" data-bs-target="#createJobModal">
<i class="bi bi-clipboard-check me-1"></i>Create Job from Quote
</button>
@@ -2,6 +2,7 @@
@{
ViewData["Title"] = "Setup Complete!";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
var showGuidedActivationCta = (bool?)ViewBag.ShowGuidedActivationCta ?? false;
}
@section Styles {
@@ -79,24 +80,28 @@
<p style="color:rgba(255,255,255,0.8);font-size:1.05rem;max-width:500px;margin:0 auto 1.5rem;">
Your setup is complete. @progress.DoneSteps.Count of @WizardProgressDto.TotalSteps steps were configured — your shop is ready to roll.
</p>
<a asp-controller="Dashboard" asp-action="Index" class="btn btn-light btn-lg px-5 fw-semibold">
<i class="bi bi-house me-2"></i>Go to Dashboard
</a>
@if (showGuidedActivationCta)
{
<a asp-controller="GuidedActivation" asp-action="Start" class="btn btn-light btn-lg px-5 fw-semibold">
<i class="bi bi-play-circle me-2"></i>Start First Workflow
</a>
}
else
{
<a asp-controller="Dashboard" asp-action="Index" class="btn btn-light btn-lg px-5 fw-semibold">
<i class="bi bi-house me-2"></i>Go to Dashboard
</a>
}
</div>
@{
var stepLabels = new Dictionary<int, (string Label, string Icon)>
{
{ 1, ("Company Profile", "bi-building") },
{ 2, ("QB Migration", "bi-arrow-left-right") },
{ 3, ("Operating Costs", "bi-currency-dollar") },
{ 4, ("Shop Ovens", "bi-fire") },
{ 5, ("Doc Numbering", "bi-palette") },
{ 6, ("Job Settings", "bi-diagram-3") },
{ 7, ("Payment Terms", "bi-file-earmark-text") },
{ 8, ("Pricing Tiers", "bi-percent") },
{ 9, ("Notifications", "bi-bell") },
{ 10, ("Team Members", "bi-people") },
{ 1, ("Company Profile", "bi-building") },
{ 2, ("QB Migration", "bi-arrow-left-right") },
{ 3, ("Operating Costs", "bi-currency-dollar") },
{ 4, ("Shop Ovens", "bi-fire") },
{ 5, ("Notifications", "bi-bell") },
};
}
@@ -146,7 +151,16 @@
<a asp-controller="CompanySettings" asp-action="Index" class="btn btn-outline-primary">
<i class="bi bi-gear me-1"></i>Open Company Settings
</a>
<a asp-controller="Dashboard" asp-action="Index" class="btn btn-primary">
<i class="bi bi-house me-1"></i>Go to Dashboard
</a>
@if (showGuidedActivationCta)
{
<a asp-controller="GuidedActivation" asp-action="Start" class="btn btn-primary">
<i class="bi bi-play-circle me-1"></i>Start First Workflow
</a>
}
else
{
<a asp-controller="Dashboard" asp-action="Index" class="btn btn-primary">
<i class="bi bi-house me-1"></i>Go to Dashboard
</a>
}
</div>
@@ -19,8 +19,10 @@
<form asp-action="PostStep4" method="post" onsubmit="return validateStep4()">
@Html.AntiForgeryToken()
<input type="hidden" name="OvensJson" id="ovensJson" value="@Html.Raw(Model.OvensJson ?? "[]")" />
<input type="hidden" name="BlastSetupsJson" id="blastSetupsJson" value="@Html.Raw(Model.BlastSetupsJson ?? "[]")" />
<script type="application/json" id="ovensSeedJson">@Html.Raw(Model.OvensJson ?? "[]")</script>
<script type="application/json" id="blastSetupsSeedJson">@Html.Raw(Model.BlastSetupsJson ?? "[]")</script>
<input type="hidden" name="OvensJson" id="ovensJson" value="[]" />
<input type="hidden" name="BlastSetupsJson" id="blastSetupsJson" value="[]" />
<!-- ── Ovens ─────────────────────────────────────────────────────── -->
<div class="wizard-card">
@@ -75,7 +77,7 @@
// ═══════════════════════════════════════════════════════════════════════════
// OVENS
// ═══════════════════════════════════════════════════════════════════════════
var ovens = JSON.parse(document.getElementById('ovensJson').value || '[]');
var ovens = JSON.parse(document.getElementById('ovensSeedJson').textContent || '[]');
function serializeOvens() {
document.getElementById('ovensJson').value = JSON.stringify(
@@ -212,7 +214,7 @@
// ═══════════════════════════════════════════════════════════════════════════
// BLAST SETUPS
// ═══════════════════════════════════════════════════════════════════════════
var blasts = JSON.parse(document.getElementById('blastSetupsJson').value || '[]');
var blasts = JSON.parse(document.getElementById('blastSetupsSeedJson').textContent || '[]');
function serializeBlasts() {
document.getElementById('blastSetupsJson').value = JSON.stringify(
@@ -3,7 +3,7 @@
@{
ViewData["Title"] = "Setup Wizard — Notifications";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 9;
int step = ViewBag.Step as int? ?? 5;
}
@section Styles { @await Html.PartialAsync("_WizardStyles") }
@@ -4,16 +4,11 @@
@{
var steps = new[]
{
(1, "Company Profile", "bi-building"),
(2, "QB Migration", "bi-arrow-left-right"),
(3, "Operating Costs", "bi-currency-dollar"),
(4, "Shop Ovens", "bi-fire"),
(5, "Doc Numbering", "bi-palette"),
(6, "Job Settings", "bi-diagram-3"),
(7, "Payment Terms", "bi-file-earmark-text"),
(8, "Pricing Tiers", "bi-percent"),
(9, "Notifications", "bi-bell"),
(10, "Team Members", "bi-people"),
(1, "Company Profile", "bi-building"),
(2, "QB Migration", "bi-arrow-left-right"),
(3, "Operating Costs", "bi-currency-dollar"),
(4, "Shop Ovens", "bi-fire"),
(5, "Notifications", "bi-bell"),
};
int currentStep = ViewBag.Step as int? ?? 1;
}