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:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user