Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -0,0 +1,152 @@
@using PowderCoating.Application.DTOs.Wizard
@{
ViewData["Title"] = "Setup Complete!";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
}
@section Styles {
<style>
.complete-hero {
background: linear-gradient(135deg, #065f46 0%, #047857 50%, #059669 100%);
border-radius: 1rem;
color: white;
padding: 3rem 2rem;
text-align: center;
margin-bottom: 2rem;
position: relative;
overflow: hidden;
}
.complete-hero::after {
content: '\f26b';
font-family: 'bootstrap-icons';
position: absolute;
right: -1rem;
top: -1rem;
font-size: 12rem;
color: rgba(255,255,255,0.05);
pointer-events: none;
}
.complete-hero h1 {
font-size: 2rem;
font-weight: 800;
margin-bottom: 0.5rem;
}
.steps-done-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.step-done-tile {
background: var(--bs-body-bg);
border: 1px solid var(--bs-border-color);
border-radius: 0.75rem;
padding: 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.875rem;
}
.step-done-tile.done { border-color: #d1fae5; }
.step-done-tile.skipped { opacity: 0.65; }
.step-done-icon {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.9rem;
flex-shrink: 0;
}
.step-done-icon.done { background: #d1fae5; color: #059669; }
.step-done-icon.skipped { background: var(--bs-tertiary-bg); color: var(--bs-secondary-color); }
</style>
}
<div class="complete-hero">
<div class="mb-3">
<i class="bi bi-check-circle-fill" style="font-size:3rem;"></i>
</div>
<h1>You're all set!</h1>
<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>
</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") },
};
}
<h5 class="fw-bold mb-3">Steps Summary</h5>
<div class="steps-done-grid">
@for (int i = 1; i <= WizardProgressDto.TotalSteps; i++)
{
var (label, icon) = stepLabels[i];
bool done = progress.IsStepDone(i);
bool skipped = progress.IsStepSkipped(i) && !done;
var stateClass = done ? "done" : skipped ? "skipped" : "skipped";
var iconClass = done ? "done" : "skipped";
<div class="step-done-tile @stateClass">
<div class="step-done-icon @iconClass">
@if (done)
{
<i class="bi bi-check-lg"></i>
}
else
{
<i class="bi bi-dash"></i>
}
</div>
<div>
<div class="fw-semibold">@label</div>
<div class="text-secondary" style="font-size:0.8rem;">@(done ? "Completed" : "Skipped")</div>
</div>
</div>
}
</div>
@if (progress.SkippedSteps.Any(s => !progress.IsStepDone(s)))
{
<div class="alert alert-warning d-flex gap-2 mb-4">
<i class="bi bi-exclamation-triangle-fill flex-shrink-0 mt-1"></i>
<div>
You skipped @progress.SkippedSteps.Count(s => !progress.IsStepDone(s)) step(s). You can always re-run the setup wizard from
<a asp-controller="CompanySettings" asp-action="Index">Company Settings</a>.
</div>
</div>
}
<div class="d-flex gap-2 flex-wrap">
<a asp-controller="SetupWizard" asp-action="Step" asp-route-step="1" class="btn btn-outline-secondary">
<i class="bi bi-arrow-counterclockwise me-1"></i>Re-run Wizard
</a>
<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>
</div>
@@ -0,0 +1,124 @@
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep1Dto
@{
ViewData["Title"] = "Setup Wizard — Company Profile";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 1;
}
@section Styles {
@await Html.PartialAsync("_WizardStyles")
}
<div class="wizard-layout">
@await Html.PartialAsync("_WizardProgress", progress)
<div class="wizard-content">
<div class="wizard-step-header">
<span class="wizard-step-badge">Step @step of @WizardProgressDto.TotalSteps</span>
<h2><i class="bi bi-building me-2"></i>Company Profile</h2>
<p class="text-secondary">Tell us about your business. This information appears on quotes, invoices, and emails you send.</p>
</div>
<div class="alert alert-info alert-permanent d-flex align-items-start gap-2 mb-3" role="alert">
<i class="bi bi-info-circle-fill flex-shrink-0 mt-1"></i>
<div class="small">
<strong>Where this information is used:</strong>
<ul class="mb-0 mt-1 ps-3">
<li><strong>Quote &amp; invoice PDFs</strong> — your company name, address, and phone appear in the header of every document sent to customers.</li>
<li><strong>Customer-facing emails</strong> — the primary contact name and email are used as reply-to information in outgoing notifications.</li>
<li><strong>Timezone</strong> — affects when scheduled reminders and alerts fire. Set it to your shop's local time.</li>
<li><strong>Currency &amp; units</strong> — all pricing and surface area calculations throughout the system will use these preferences.</li>
</ul>
</div>
</div>
<form asp-action="PostStep1" method="post">
@Html.AntiForgeryToken()
<div class="wizard-card">
<h5 class="wizard-card-title">Basic Information</h5>
<div class="row g-3">
<div class="col-md-8">
<label asp-for="CompanyName" class="form-label fw-semibold"></label>
<input asp-for="CompanyName" class="form-control" placeholder="e.g. Acme Powder Coating LLC" />
<span asp-validation-for="CompanyName" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="Phone" class="form-label fw-semibold"></label>
<input asp-for="Phone" class="form-control" placeholder="(555) 555-5555" />
<span asp-validation-for="Phone" class="text-danger small"></span>
</div>
<div class="col-md-6">
<label asp-for="PrimaryContactName" class="form-label fw-semibold"></label>
<input asp-for="PrimaryContactName" class="form-control" placeholder="Your full name" />
</div>
<div class="col-md-6">
<label asp-for="PrimaryContactEmail" class="form-label fw-semibold"></label>
<input asp-for="PrimaryContactEmail" class="form-control" placeholder="owner@yourcompany.com" />
<span asp-validation-for="PrimaryContactEmail" class="text-danger small"></span>
</div>
</div>
</div>
<div class="wizard-card">
<h5 class="wizard-card-title">Address</h5>
<div class="row g-3">
<div class="col-12">
<label asp-for="Address" class="form-label fw-semibold"></label>
<input asp-for="Address" class="form-control" placeholder="123 Main Street" />
</div>
<div class="col-md-5">
<label asp-for="City" class="form-label fw-semibold"></label>
<input asp-for="City" class="form-control" placeholder="City" />
</div>
<div class="col-md-3">
<label asp-for="State" class="form-label fw-semibold"></label>
<input asp-for="State" class="form-control" placeholder="FL" maxlength="2" />
</div>
<div class="col-md-4">
<label asp-for="ZipCode" class="form-label fw-semibold"></label>
<input asp-for="ZipCode" class="form-control" placeholder="33101" />
</div>
<div class="col-md-6">
<label asp-for="TimeZone" class="form-label fw-semibold"></label>
<select asp-for="TimeZone" class="form-select">
<option value="America/New_York">Eastern (ET)</option>
<option value="America/Chicago">Central (CT)</option>
<option value="America/Denver">Mountain (MT)</option>
<option value="America/Los_Angeles">Pacific (PT)</option>
<option value="America/Anchorage">Alaska (AKT)</option>
<option value="Pacific/Honolulu">Hawaii (HAT)</option>
</select>
</div>
</div>
</div>
<div class="wizard-card">
<h5 class="wizard-card-title">Regional Settings</h5>
<div class="row g-3">
<div class="col-md-4">
<label asp-for="DefaultCurrency" class="form-label fw-semibold"></label>
<select asp-for="DefaultCurrency" class="form-select">
<option value="USD">USD — US Dollar ($)</option>
<option value="CAD">CAD — Canadian Dollar (C$)</option>
<option value="EUR">EUR — Euro (€)</option>
<option value="GBP">GBP — British Pound (£)</option>
<option value="AUD">AUD — Australian Dollar (A$)</option>
<option value="MXN">MXN — Mexican Peso (MX$)</option>
</select>
</div>
<div class="col-md-8 d-flex align-items-end pb-1">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" asp-for="UseMetricSystem" id="UseMetricSystem" />
<label class="form-check-label fw-semibold" for="UseMetricSystem">@Html.DisplayNameFor(m => m.UseMetricSystem)</label>
<div class="form-text mt-1">Off = imperial (ft², lb) &nbsp;|&nbsp; On = metric (m², kg)</div>
</div>
</div>
</div>
</div>
@await Html.PartialAsync("_WizardFooter", step)
</form>
</div>
</div>
@@ -0,0 +1,148 @@
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep9Dto
@{
ViewData["Title"] = "Setup Wizard — Team Members";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 10;
}
@section Styles { @await Html.PartialAsync("_WizardStyles") }
<div class="wizard-layout">
@await Html.PartialAsync("_WizardProgress", progress)
<div class="wizard-content">
<div class="wizard-step-header">
<span class="wizard-step-badge">Step @step of @WizardProgressDto.TotalSteps</span>
<h2><i class="bi bi-people me-2"></i>Team Members</h2>
<p class="text-secondary">Add your team so they can log in and start working right away. You can add or edit users anytime from Company Settings &rarr; Users.</p>
</div>
@{
var existingMembers = ViewBag.ExistingTeamMembers as IEnumerable<dynamic> ?? Enumerable.Empty<dynamic>();
}
@if (existingMembers.Any())
{
<div class="wizard-card mb-3">
<h5 class="wizard-card-title"><i class="bi bi-people-fill me-2 text-success"></i>Existing Team Members</h5>
<p class="text-secondary small mb-2">These users are already set up. You can manage them in <strong>Company Settings → Users</strong>.</p>
<ul class="list-group list-group-flush">
@foreach (var m in existingMembers)
{
<li class="list-group-item px-0 py-1 d-flex justify-content-between align-items-center">
<span><i class="bi bi-person-circle me-2 text-secondary"></i>@m.FirstName @m.LastName — <span class="text-secondary">@m.Email</span></span>
<span class="badge bg-secondary">@m.CompanyRole</span>
</li>
}
</ul>
</div>
}
<form asp-action="PostStep10" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="MembersJson" id="membersJson" value="[]" />
<div class="wizard-card">
<h5 class="wizard-card-title">Add New Team Members</h5>
<div id="membersList"></div>
<button type="button" class="btn btn-outline-primary btn-sm mt-2" onclick="addMember()">
<i class="bi bi-person-plus me-1"></i>Add Team Member
</button>
<div class="alert alert-info alert-permanent d-flex gap-2 mt-3 mb-0" role="alert">
<i class="bi bi-shield-check flex-shrink-0 mt-1"></i>
<div class="small">
<strong>Password requirements:</strong> at least 8 characters with uppercase, lowercase, and a number.
Team members will be able to change their password after logging in.
</div>
</div>
</div>
@await Html.PartialAsync("_WizardFooter", step)
</form>
</div>
</div>
@section Scripts {
<script>
var members = [];
var roleOptions = [
{ value: 'Worker', label: 'Worker (Shop Floor)' },
{ value: 'Manager', label: 'Manager' },
{ value: 'CompanyAdmin', label: 'Company Admin' },
{ value: 'Viewer', label: 'Read-Only Viewer' }
];
function renderMembers() {
var container = document.getElementById('membersList');
if (members.length === 0) {
container.innerHTML = '<p class="text-secondary small py-2">No team members added yet. You can skip this step and add users later.</p>';
} else {
container.innerHTML = members.map(function (m, idx) {
var roleOpts = roleOptions.map(r =>
`<option value="${r.value}"${m.companyRole === r.value ? ' selected' : ''}>${r.label}</option>`
).join('');
return `<div class="wz-item-row">
<div class="row g-2 align-items-end">
<div class="col-md-3">
<label class="form-label small fw-semibold mb-1">First Name</label>
<input class="form-control form-control-sm" value="${escHtml(m.firstName)}" onchange="updateMember(${idx},'firstName',this.value)" placeholder="Jane" />
</div>
<div class="col-md-3">
<label class="form-label small fw-semibold mb-1">Last Name</label>
<input class="form-control form-control-sm" value="${escHtml(m.lastName)}" onchange="updateMember(${idx},'lastName',this.value)" placeholder="Smith" />
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold mb-1">Email <span class="text-danger">*</span></label>
<input class="form-control form-control-sm" type="email" value="${escHtml(m.email)}" onchange="updateMember(${idx},'email',this.value)" placeholder="jane@company.com" />
</div>
<div class="col-md-2 text-end">
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeMember(${idx})" title="Remove">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<div class="row g-2 mt-1">
<div class="col-md-4">
<label class="form-label small fw-semibold mb-1">Password <span class="text-danger">*</span></label>
<input class="form-control form-control-sm" type="password" value="${escHtml(m.password)}" onchange="updateMember(${idx},'password',this.value)" placeholder="Min 8 chars, mixed case + number" />
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold mb-1">Role</label>
<select class="form-select form-select-sm" onchange="updateMember(${idx},'companyRole',this.value)">
${roleOpts}
</select>
</div>
</div>
</div>`;
}).join('');
}
document.getElementById('membersJson').value = JSON.stringify(members);
}
function addMember() {
members.push({ firstName: '', lastName: '', email: '', password: '', companyRole: 'Worker' });
renderMembers();
var inputs = document.querySelectorAll('.wz-item-row:last-child input');
if (inputs.length) inputs[0].focus();
}
function updateMember(idx, field, value) {
members[idx][field] = value;
document.getElementById('membersJson').value = JSON.stringify(members);
}
function removeMember(idx) {
members.splice(idx, 1);
renderMembers();
}
function escHtml(str) {
return (str || '').toString().replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
renderMembers();
</script>
}
@@ -0,0 +1,65 @@
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep6Dto
@{
ViewData["Title"] = "Setup Wizard — Chart of Accounts";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 11;
}
@section Styles { @await Html.PartialAsync("_WizardStyles") }
<div class="wizard-layout">
@await Html.PartialAsync("_WizardProgress", progress)
<div class="wizard-content">
<div class="wizard-step-header">
<span class="wizard-step-badge">Step @step of @WizardProgressDto.TotalSteps</span>
<h2><i class="bi bi-bar-chart me-2"></i>Chart of Accounts</h2>
<p class="text-secondary">Your Chart of Accounts has been pre-loaded with standard accounts for a powder coating business. Review them below.</p>
</div>
<div class="wizard-card">
<h5 class="wizard-card-title">Account Summary</h5>
<p class="text-secondary mb-4">These accounts are used throughout the system to categorize expenses, revenue, and inventory. You can manage them in detail from <strong>Accounting &rarr; Chart of Accounts</strong>.</p>
<div class="row g-3 mb-4">
<div class="col-6 col-md-3">
<div class="text-center p-3 rounded border">
<div class="fs-2 fw-bold text-primary">@Model.AccountCount</div>
<div class="text-secondary small">Total Accounts</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="text-center p-3 rounded border">
<div class="fs-2 fw-bold text-success">@Model.RevenueAccounts</div>
<div class="text-secondary small">Revenue Accounts</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="text-center p-3 rounded border">
<div class="fs-2 fw-bold text-danger">@Model.ExpenseAccounts</div>
<div class="text-secondary small">Expense Accounts</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="text-center p-3 rounded border">
<div class="fs-2 fw-bold text-info">@Model.AssetAccounts</div>
<div class="text-secondary small">Asset Accounts</div>
</div>
</div>
</div>
<div class="alert alert-info alert-permanent d-flex align-items-start gap-2 mb-0" role="alert">
<i class="bi bi-info-circle-fill mt-1 flex-shrink-0"></i>
<div>
<strong>Already set up for you.</strong> No action needed here. You can review and customize accounts anytime from the Accounting module.
<a asp-controller="Accounts" asp-action="Index" class="ms-1">View Chart of Accounts &rarr;</a>
</div>
</div>
</div>
<form asp-action="PostStep11" method="post">
@Html.AntiForgeryToken()
@await Html.PartialAsync("_WizardFooter", step)
</form>
</div>
</div>
@@ -0,0 +1,112 @@
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep10Dto
@{
ViewData["Title"] = "Setup Wizard — Vendors & Suppliers";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 12;
}
@section Styles { @await Html.PartialAsync("_WizardStyles") }
<div class="wizard-layout">
@await Html.PartialAsync("_WizardProgress", progress)
<div class="wizard-content">
<div class="wizard-step-header">
<span class="wizard-step-badge">Step @step of @WizardProgressDto.TotalSteps</span>
<h2><i class="bi bi-truck me-2"></i>Vendors &amp; Suppliers</h2>
<p class="text-secondary">Vendors are your powder coating suppliers, material sources, and service providers. They appear in inventory purchasing, bills, and expense tracking. Adding them now means they'll be available immediately when you start creating purchase orders or logging expenses.</p>
</div>
<form asp-action="PostStep12" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="VendorsJson" id="vendorsJson" value="[]" />
<div class="wizard-card">
<h5 class="wizard-card-title">Vendor List</h5>
<p class="text-secondary small mb-3">We've suggested two popular powder coating suppliers to get you started — keep, edit, or remove them as needed. You can add more vendors anytime from the Vendors section.</p>
<div id="vendorsList"></div>
<button type="button" class="btn btn-outline-primary btn-sm mt-2" onclick="addVendor()">
<i class="bi bi-plus-circle me-1"></i>Add Vendor
</button>
</div>
@await Html.PartialAsync("_WizardFooter", step)
</form>
</div>
</div>
@section Scripts {
<script>
var vendors = [
{ companyName: 'Prismatic Powders', contactName: '', email: '', phone: '', website: 'https://www.prismaticpowders.com' },
{ companyName: 'Columbia Coatings', contactName: '', email: '', phone: '', website: 'https://www.columbiacoatings.com' }
];
function escHtml(str) {
return (str || '').toString().replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function renderVendors() {
var container = document.getElementById('vendorsList');
if (vendors.length === 0) {
container.innerHTML = '<p class="text-secondary small py-2">No vendors added yet. You can skip this step and add vendors later.</p>';
} else {
container.innerHTML = vendors.map(function (v, idx) {
return `<div class="wz-item-row">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label small fw-semibold mb-1">Company Name <span class="text-danger">*</span></label>
<input class="form-control form-control-sm" value="${escHtml(v.companyName)}" onchange="updateVendor(${idx},'companyName',this.value)" placeholder="e.g. Prismatic Powders" />
</div>
<div class="col-md-3">
<label class="form-label small fw-semibold mb-1">Contact Name</label>
<input class="form-control form-control-sm" value="${escHtml(v.contactName)}" onchange="updateVendor(${idx},'contactName',this.value)" placeholder="Jane Smith" />
</div>
<div class="col-md-3">
<label class="form-label small fw-semibold mb-1">Phone</label>
<input class="form-control form-control-sm" value="${escHtml(v.phone)}" onchange="updateVendor(${idx},'phone',this.value)" placeholder="(555) 000-0000" />
</div>
<div class="col-md-2 text-end">
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeVendor(${idx})" title="Remove">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<div class="row g-2 mt-1">
<div class="col-md-4">
<label class="form-label small fw-semibold mb-1">Email</label>
<input class="form-control form-control-sm" type="email" value="${escHtml(v.email)}" onchange="updateVendor(${idx},'email',this.value)" placeholder="orders@vendor.com" />
</div>
<div class="col-md-5">
<label class="form-label small fw-semibold mb-1">Website</label>
<input class="form-control form-control-sm" value="${escHtml(v.website)}" onchange="updateVendor(${idx},'website',this.value)" placeholder="https://www.vendor.com" />
</div>
</div>
</div>`;
}).join('');
}
document.getElementById('vendorsJson').value = JSON.stringify(vendors);
}
function addVendor() {
vendors.push({ companyName: '', contactName: '', email: '', phone: '', website: '' });
renderVendors();
var inputs = document.querySelectorAll('.wz-item-row:last-child input');
if (inputs.length) inputs[0].focus();
}
function updateVendor(idx, field, value) {
vendors[idx][field] = value;
document.getElementById('vendorsJson').value = JSON.stringify(vendors);
}
function removeVendor(idx) {
vendors.splice(idx, 1);
renderVendors();
}
renderVendors();
</script>
}
@@ -0,0 +1,134 @@
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep8Dto
@{
ViewData["Title"] = "Setup Wizard — Inventory / Powder Colors";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 13;
}
@section Styles { @await Html.PartialAsync("_WizardStyles") }
<div class="wizard-layout">
@await Html.PartialAsync("_WizardProgress", progress)
<div class="wizard-content">
<div class="wizard-step-header">
<span class="wizard-step-badge">Step @step of @WizardProgressDto.TotalSteps</span>
<h2><i class="bi bi-box-seam me-2"></i>Inventory / Powder Colors</h2>
<p class="text-secondary">Add the powder colors you stock so they're available when building quotes and jobs. You can add more anytime from the Inventory module.</p>
</div>
<div class="alert alert-info alert-permanent d-flex align-items-start gap-2 mb-3" role="alert">
<i class="bi bi-info-circle-fill flex-shrink-0 mt-1"></i>
<div class="small">
<strong>Why add inventory during setup?</strong>
<ul class="mb-0 mt-1 ps-3">
<li><strong>Powder cost calculations</strong> — when unit cost is set, the system automatically calculates the exact powder cost for each coat on a quote based on surface area, coverage rate, and transfer efficiency.</li>
<li><strong>Powder Needed display</strong> — quotes and jobs show how many pounds of each color to pull from your shelf, reducing waste and over-ordering.</li>
<li><strong>Low-stock alerts</strong> — set reorder points so you're notified before you run out of a color in the middle of a job.</li>
<li><strong>Quick selection in quotes</strong> — inventory colors appear in the coating dropdown when building quotes, avoiding typos and ensuring consistency.</li>
</ul>
<span class="d-block mt-1 text-secondary">Enter your most-used colors now and add the rest later. You can also import inventory from a CSV file in the Inventory module.</span>
</div>
</div>
<form asp-action="PostStep13" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="ItemsJson" id="itemsJson" value="[]" />
<div class="wizard-card">
<h5 class="wizard-card-title">Powder Colors</h5>
<div id="itemsList"></div>
<button type="button" class="btn btn-outline-primary btn-sm mt-2" onclick="addItem()">
<i class="bi bi-plus-circle me-1"></i>Add Powder Color
</button>
<p class="text-secondary small mt-3 mb-0">
<i class="bi bi-info-circle me-1"></i>
Tip: You can also import inventory from CSV in the Inventory module after setup.
</p>
</div>
@await Html.PartialAsync("_WizardFooter", step)
</form>
</div>
</div>
@section Scripts {
<script>
var items = [];
function renderItems() {
var container = document.getElementById('itemsList');
if (items.length === 0) {
container.innerHTML = '<p class="text-secondary small py-2">No items added yet. Click "Add Powder Color" to get started.</p>';
} else {
container.innerHTML = items.map(function (item, idx) {
return `<div class="wz-item-row">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label small fw-semibold mb-1">Color Name <span class="text-danger">*</span></label>
<input class="form-control form-control-sm" value="${escHtml(item.name)}" onchange="updateItem(${idx},'name',this.value)" placeholder="e.g. Gloss Black" />
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold mb-1">Color Code</label>
<input class="form-control form-control-sm" value="${escHtml(item.colorCode)}" onchange="updateItem(${idx},'colorCode',this.value)" placeholder="RAL 9005" />
</div>
<div class="col-md-3">
<label class="form-label small fw-semibold mb-1">Manufacturer</label>
<input class="form-control form-control-sm" value="${escHtml(item.manufacturer)}" onchange="updateItem(${idx},'manufacturer',this.value)" placeholder="Sherwin-Williams" />
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold mb-1">Finish</label>
<select class="form-select form-select-sm" onchange="updateItem(${idx},'finish',this.value)">
${['Gloss','Semi-Gloss','Matte','Flat','Satin','Metallic','Textured'].map(f => `<option value="${f}"${item.finish===f?' selected':''}>${f}</option>`).join('')}
</select>
</div>
<div class="col-md-1 text-end">
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeItem(${idx})" title="Remove">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<div class="row g-2 mt-1">
<div class="col-md-3">
<label class="form-label small fw-semibold mb-1">Unit Cost ($/lb)</label>
<input class="form-control form-control-sm" type="number" min="0" step="0.01" value="${item.unitCost || ''}" onchange="updateItem(${idx},'unitCost',parseFloat(this.value)||0)" placeholder="0.00" />
</div>
<div class="col-md-3">
<label class="form-label small fw-semibold mb-1">Qty on Hand (lbs)</label>
<input class="form-control form-control-sm" type="number" min="0" step="0.1" value="${item.quantityOnHand || ''}" onchange="updateItem(${idx},'quantityOnHand',parseFloat(this.value)||0)" placeholder="0" />
</div>
</div>
</div>`;
}).join('');
}
document.getElementById('itemsJson').value = JSON.stringify(items);
}
function addItem() {
items.push({ name: '', colorName: '', colorCode: '', manufacturer: '', finish: 'Gloss', unitCost: 0, quantityOnHand: 0 });
renderItems();
var inputs = document.querySelectorAll('.wz-item-row:last-child input');
if (inputs.length) inputs[0].focus();
}
function updateItem(idx, field, value) {
items[idx][field] = value;
if (field === 'name') items[idx].colorName = value;
document.getElementById('itemsJson').value = JSON.stringify(items);
}
function removeItem(idx) {
items.splice(idx, 1);
renderItems();
}
function escHtml(str) {
return (str || '').toString().replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
renderItems();
</script>
}
@@ -0,0 +1,117 @@
@using PowderCoating.Application.DTOs.Wizard
@model WizardOvensStepDto
@{
ViewData["Title"] = "Setup Wizard — Equipment & Ovens";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 14;
}
@section Styles { @await Html.PartialAsync("_WizardStyles") }
<div class="wizard-layout">
@await Html.PartialAsync("_WizardProgress", progress)
<div class="wizard-content">
<div class="wizard-step-header">
<span class="wizard-step-badge">Step @step of @WizardProgressDto.TotalSteps</span>
<h2><i class="bi bi-fire me-2"></i>Equipment &amp; Ovens</h2>
<p class="text-secondary">Register your ovens so the Oven Scheduler can track capacity and plan batches. You can add other equipment (spray booths, compressors) from the Equipment section later.</p>
</div>
<form asp-action="PostStep14" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="OvensJson" id="ovensJson" value="[]" />
<div class="wizard-card">
<h5 class="wizard-card-title">Named Ovens</h5>
<p class="text-secondary small mb-3">
Each oven entry appears in the Oven Scheduler's capacity planner. Set the maximum load (sq ft) and typical cycle time so the scheduler can estimate how many batches fit in a day.
</p>
<div id="ovensList"></div>
<button type="button" class="btn btn-outline-primary btn-sm mt-2" onclick="addOven()">
<i class="bi bi-plus-circle me-1"></i>Add Oven
</button>
<div class="alert alert-info alert-permanent d-flex gap-2 mt-3 mb-0" role="alert">
<i class="bi bi-info-circle flex-shrink-0 mt-1"></i>
<div class="small">
You can skip this step and configure equipment later from <strong>Operations &rarr; Equipment</strong>. However, the Oven Scheduler requires at least one named oven to create batches.
</div>
</div>
</div>
@await Html.PartialAsync("_WizardFooter", step)
</form>
</div>
</div>
@section Scripts {
<script>
var ovens = [];
function renderOvens() {
var container = document.getElementById('ovensList');
if (ovens.length === 0) {
container.innerHTML = '<p class="text-secondary small py-2">No ovens added yet. You can skip this step and add equipment later.</p>';
} else {
container.innerHTML = ovens.map(function (o, idx) {
return `<div class="wz-item-row">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label small fw-semibold mb-1">Oven Name <span class="text-danger">*</span></label>
<input class="form-control form-control-sm" value="${escHtml(o.label)}" onchange="updateOven(${idx},'label',this.value)" placeholder="e.g. Main Oven, Oven #2" />
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold mb-1">Cost/hr ($)</label>
<input class="form-control form-control-sm" type="number" min="0" step="0.01" value="${o.costPerHour || 0}" onchange="updateOvenNum(${idx},'costPerHour',this.value)" />
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold mb-1">Max Load (sq ft)</label>
<input class="form-control form-control-sm" type="number" min="0" step="0.1" value="${o.maxLoadSqFt || ''}" onchange="updateOvenNum(${idx},'maxLoadSqFt',this.value)" placeholder="e.g. 80" />
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold mb-1">Cycle Time (min)</label>
<input class="form-control form-control-sm" type="number" min="1" step="1" value="${o.defaultCycleMinutes || ''}" onchange="updateOvenNum(${idx},'defaultCycleMinutes',this.value)" placeholder="e.g. 25" />
</div>
<div class="col-md-2 text-end">
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeOven(${idx})" title="Remove">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>`;
}).join('');
}
document.getElementById('ovensJson').value = JSON.stringify(ovens);
}
function addOven() {
ovens.push({ label: '', costPerHour: 0, maxLoadSqFt: null, defaultCycleMinutes: null });
renderOvens();
var inputs = document.querySelectorAll('.wz-item-row:last-child input');
if (inputs.length) inputs[0].focus();
}
function updateOven(idx, field, value) {
ovens[idx][field] = value;
document.getElementById('ovensJson').value = JSON.stringify(ovens);
}
function updateOvenNum(idx, field, value) {
ovens[idx][field] = value === '' ? null : parseFloat(value);
document.getElementById('ovensJson').value = JSON.stringify(ovens);
}
function removeOven(idx) {
ovens.splice(idx, 1);
renderOvens();
}
function escHtml(str) {
return (str || '').toString().replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
renderOvens();
</script>
}
@@ -0,0 +1,110 @@
@using PowderCoating.Application.DTOs.Wizard
@model WizardPricingTiersStepDto
@{
ViewData["Title"] = "Setup Wizard — Pricing Tiers";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 15;
}
@section Styles { @await Html.PartialAsync("_WizardStyles") }
<div class="wizard-layout">
@await Html.PartialAsync("_WizardProgress", progress)
<div class="wizard-content">
<div class="wizard-step-header">
<span class="wizard-step-badge">Step @step of @WizardProgressDto.TotalSteps</span>
<h2><i class="bi bi-percent me-2"></i>Pricing Tiers</h2>
<p class="text-secondary">Create discount tiers for your commercial customers. Once set up, you can assign a tier to any customer so their quotes automatically apply the discount.</p>
</div>
<form asp-action="PostStep15" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="TiersJson" id="tiersJson" value="[]" />
<div class="wizard-card">
<h5 class="wizard-card-title">Pricing Tiers</h5>
<p class="text-secondary small mb-3">
Common examples: <em>Gold (10% off)</em>, <em>Silver (5% off)</em>, <em>Wholesale (15% off)</em>.
Customers without a tier are billed at standard rates.
</p>
<div id="tiersList"></div>
<button type="button" class="btn btn-outline-primary btn-sm mt-2" onclick="addTier()">
<i class="bi bi-plus-circle me-1"></i>Add Pricing Tier
</button>
</div>
@await Html.PartialAsync("_WizardFooter", step)
</form>
</div>
</div>
@section Scripts {
<script>
var tiers = [];
function renderTiers() {
var container = document.getElementById('tiersList');
if (tiers.length === 0) {
container.innerHTML = '<p class="text-secondary small py-2">No tiers added yet. You can skip this step and set up pricing tiers later from Settings &rarr; Pricing Tiers.</p>';
} else {
container.innerHTML = tiers.map(function (t, idx) {
return `<div class="wz-item-row">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label small fw-semibold mb-1">Tier Name <span class="text-danger">*</span></label>
<input class="form-control form-control-sm" value="${escHtml(t.tierName)}" onchange="updateTier(${idx},'tierName',this.value)" placeholder="e.g. Gold, Wholesale" />
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold mb-1">Description</label>
<input class="form-control form-control-sm" value="${escHtml(t.description)}" onchange="updateTier(${idx},'description',this.value)" placeholder="e.g. High-volume commercial accounts" />
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold mb-1">Discount (%)</label>
<div class="input-group input-group-sm">
<input class="form-control form-control-sm" type="number" min="0" max="100" step="0.1" value="${t.discountPercent || 0}" onchange="updateTierNum(${idx},'discountPercent',this.value)" />
<span class="input-group-text">%</span>
</div>
</div>
<div class="col-md-2 text-end">
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeTier(${idx})" title="Remove">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>`;
}).join('');
}
document.getElementById('tiersJson').value = JSON.stringify(tiers);
}
function addTier() {
tiers.push({ tierName: '', description: '', discountPercent: 0 });
renderTiers();
var inputs = document.querySelectorAll('.wz-item-row:last-child input');
if (inputs.length) inputs[0].focus();
}
function updateTier(idx, field, value) {
tiers[idx][field] = value;
document.getElementById('tiersJson').value = JSON.stringify(tiers);
}
function updateTierNum(idx, field, value) {
tiers[idx][field] = value === '' ? 0 : parseFloat(value);
document.getElementById('tiersJson').value = JSON.stringify(tiers);
}
function removeTier(idx) {
tiers.splice(idx, 1);
renderTiers();
}
function escHtml(str) {
return (str || '').toString().replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
renderTiers();
</script>
}
@@ -0,0 +1,118 @@
@using PowderCoating.Application.DTOs.Wizard
@model WizardCatalogStepDto
@{
ViewData["Title"] = "Setup Wizard — Service Catalog";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 16;
}
@section Styles { @await Html.PartialAsync("_WizardStyles") }
<div class="wizard-layout">
@await Html.PartialAsync("_WizardProgress", progress)
<div class="wizard-content">
<div class="wizard-step-header">
<span class="wizard-step-badge">Step @step of @WizardProgressDto.TotalSteps</span>
<h2><i class="bi bi-grid me-2"></i>Service Catalog</h2>
<p class="text-secondary">Add your most common services as catalog items. These appear as quick-add options when building quotes and jobs, saving time on repetitive entries.</p>
</div>
<form asp-action="PostStep16" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="ItemsJson" id="catalogJson" value="[]" />
<div class="wizard-card">
<h5 class="wizard-card-title">Catalog Items</h5>
<p class="text-secondary small mb-3">
Examples: <em>Wheel (standard rim)</em>, <em>Bumper (full)</em>, <em>Roll bar</em>, <em>Frame section</em>.
You can set a default price and typical surface area so quotes calculate instantly.
</p>
<div id="catalogList"></div>
<button type="button" class="btn btn-outline-primary btn-sm mt-2" onclick="addItem()">
<i class="bi bi-plus-circle me-1"></i>Add Catalog Item
</button>
<div class="alert alert-info alert-permanent d-flex gap-2 mt-3 mb-0" role="alert">
<i class="bi bi-info-circle flex-shrink-0 mt-1"></i>
<div class="small">
All items are created under a <strong>General Services</strong> category. You can reorganize them into custom categories later from <strong>Settings &rarr; Catalog Items</strong>.
</div>
</div>
</div>
@await Html.PartialAsync("_WizardFooter", step)
</form>
</div>
</div>
@section Scripts {
<script>
var catalogItems = [];
function renderItems() {
var container = document.getElementById('catalogList');
if (catalogItems.length === 0) {
container.innerHTML = '<p class="text-secondary small py-2">No catalog items added yet. You can skip this step and build your catalog later.</p>';
} else {
container.innerHTML = catalogItems.map(function (item, idx) {
return `<div class="wz-item-row">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label small fw-semibold mb-1">Item Name <span class="text-danger">*</span></label>
<input class="form-control form-control-sm" value="${escHtml(item.name)}" onchange="updateItem(${idx},'name',this.value)" placeholder="e.g. Standard Wheel Rim" />
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold mb-1">Description</label>
<input class="form-control form-control-sm" value="${escHtml(item.description)}" onchange="updateItem(${idx},'description',this.value)" placeholder="Optional short description" />
</div>
<div class="col-md-1">
<label class="form-label small fw-semibold mb-1">Price ($)</label>
<input class="form-control form-control-sm" type="number" min="0" step="0.01" value="${item.defaultPrice || 0}" onchange="updateNum(${idx},'defaultPrice',this.value)" />
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold mb-1">Approx. Area (sq ft)</label>
<input class="form-control form-control-sm" type="number" min="0" step="0.1" value="${item.approximateArea || ''}" onchange="updateNum(${idx},'approximateArea',this.value)" placeholder="e.g. 4.5" />
</div>
<div class="col-md-1 text-end">
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeItem(${idx})" title="Remove">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>`;
}).join('');
}
document.getElementById('catalogJson').value = JSON.stringify(catalogItems);
}
function addItem() {
catalogItems.push({ name: '', description: '', defaultPrice: 0, approximateArea: null });
renderItems();
var inputs = document.querySelectorAll('.wz-item-row:last-child input');
if (inputs.length) inputs[0].focus();
}
function updateItem(idx, field, value) {
catalogItems[idx][field] = value;
document.getElementById('catalogJson').value = JSON.stringify(catalogItems);
}
function updateNum(idx, field, value) {
catalogItems[idx][field] = value === '' ? null : parseFloat(value);
document.getElementById('catalogJson').value = JSON.stringify(catalogItems);
}
function removeItem(idx) {
catalogItems.splice(idx, 1);
renderItems();
}
function escHtml(str) {
return (str || '').toString().replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
renderItems();
</script>
}
@@ -0,0 +1,136 @@
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep7Dto
@{
ViewData["Title"] = "Setup Wizard — Notifications";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 17;
}
@section Styles { @await Html.PartialAsync("_WizardStyles") }
<div class="wizard-layout">
@await Html.PartialAsync("_WizardProgress", progress)
<div class="wizard-content">
<div class="wizard-step-header">
<span class="wizard-step-badge">Step @step of @WizardProgressDto.TotalSteps</span>
<h2><i class="bi bi-bell me-2"></i>Notification Preferences</h2>
<p class="text-secondary">Configure which email notifications are sent to your customers when key events happen in the system.</p>
</div>
<form asp-action="PostStep17" method="post">
@Html.AntiForgeryToken()
<div class="wizard-card">
<h5 class="wizard-card-title">Email Sender</h5>
<div class="row g-3">
<div class="col-12">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" asp-for="EmailNotificationsEnabled" id="EmailNotificationsEnabled" />
<label class="form-check-label fw-semibold" for="EmailNotificationsEnabled">Enable Email Notifications</label>
</div>
</div>
<div class="col-md-6">
<label asp-for="EmailFromAddress" class="form-label fw-semibold"></label>
<input asp-for="EmailFromAddress" class="form-control" type="email" placeholder="noreply@yourcompany.com" />
<div class="form-text">Leave blank to use the system default sender.</div>
<span asp-validation-for="EmailFromAddress" class="text-danger small"></span>
</div>
<div class="col-md-6">
<label asp-for="EmailFromName" class="form-label fw-semibold"></label>
<input asp-for="EmailFromName" class="form-control" placeholder="Acme Powder Coating" />
<div class="form-text">The name customers see in their inbox.</div>
</div>
</div>
</div>
<div class="wizard-card">
<h5 class="wizard-card-title">Notification Events</h5>
<p class="text-secondary small mb-3">Choose which events trigger an automatic email to the customer.</p>
<div class="row g-3">
<div class="col-md-6">
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" asp-for="NotifyOnNewJob" id="NotifyOnNewJob" />
<label class="form-check-label" for="NotifyOnNewJob">New Job Created</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" asp-for="NotifyOnNewQuote" id="NotifyOnNewQuote" />
<label class="form-check-label" for="NotifyOnNewQuote">New Quote Sent</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" asp-for="NotifyOnJobStatusChange" id="NotifyOnJobStatusChange" />
<label class="form-check-label" for="NotifyOnJobStatusChange">Job Status Changes</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" asp-for="NotifyOnQuoteApproval" id="NotifyOnQuoteApproval" />
<label class="form-check-label" for="NotifyOnQuoteApproval">Quote Approved</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" asp-for="NotifyOnPaymentReceived" id="NotifyOnPaymentReceived" />
<label class="form-check-label" for="NotifyOnPaymentReceived">Payment Received</label>
</div>
</div>
</div>
</div>
<div class="wizard-card">
<h5 class="wizard-card-title">Automated Payment Reminders</h5>
<p class="text-secondary small mb-3">
Automatically send overdue invoice reminders to customers at configurable intervals.
The system checks daily and sends one reminder per threshold, per invoice.
</p>
<div class="row g-3">
<div class="col-12">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" asp-for="PaymentRemindersEnabled" id="PaymentRemindersEnabled" />
<label class="form-check-label fw-semibold" for="PaymentRemindersEnabled">Enable Automated Payment Reminders</label>
</div>
</div>
<div class="col-md-6">
<label asp-for="PaymentReminderDays" class="form-label fw-semibold"></label>
<input asp-for="PaymentReminderDays" class="form-control" placeholder="7,14,30" />
<div class="form-text">Days past the invoice due date to send a reminder (e.g. <code>7,14,30</code>).</div>
<span asp-validation-for="PaymentReminderDays" class="text-danger small"></span>
</div>
</div>
</div>
<div class="wizard-card">
<h5 class="wizard-card-title">Alert Thresholds</h5>
<p class="text-secondary small mb-3">How far in advance the system warns you about upcoming deadlines and maintenance.</p>
<div class="row g-3">
<div class="col-md-4">
<label asp-for="QuoteExpiryWarningDays" class="form-label fw-semibold"></label>
<div class="input-group">
<input asp-for="QuoteExpiryWarningDays" class="form-control" type="number" min="0" max="90" />
<span class="input-group-text">days</span>
</div>
<div class="form-text">Warn before a quote's expiry date.</div>
<span asp-validation-for="QuoteExpiryWarningDays" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="DueDateWarningDays" class="form-label fw-semibold"></label>
<div class="input-group">
<input asp-for="DueDateWarningDays" class="form-control" type="number" min="0" max="90" />
<span class="input-group-text">days</span>
</div>
<div class="form-text">Warn before a job's due date.</div>
<span asp-validation-for="DueDateWarningDays" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="MaintenanceAlertDays" class="form-label fw-semibold"></label>
<div class="input-group">
<input asp-for="MaintenanceAlertDays" class="form-control" type="number" min="0" max="90" />
<span class="input-group-text">days</span>
</div>
<div class="form-text">Warn before scheduled equipment maintenance.</div>
<span asp-validation-for="MaintenanceAlertDays" class="text-danger small"></span>
</div>
</div>
</div>
@await Html.PartialAsync("_WizardFooter", step)
</form>
</div>
</div>
@@ -0,0 +1,128 @@
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep9Dto
@{
ViewData["Title"] = "Setup Wizard — Team Members";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 18;
}
@section Styles { @await Html.PartialAsync("_WizardStyles") }
<div class="wizard-layout">
@await Html.PartialAsync("_WizardProgress", progress)
<div class="wizard-content">
<div class="wizard-step-header">
<span class="wizard-step-badge">Step @step of @WizardProgressDto.TotalSteps</span>
<h2><i class="bi bi-people me-2"></i>Team Members</h2>
<p class="text-secondary">Add your team so they can log in and start working right away. You can add or edit users anytime from Company Settings &rarr; Users.</p>
</div>
<form asp-action="PostStep18" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="MembersJson" id="membersJson" value="[]" />
<div class="wizard-card">
<h5 class="wizard-card-title">Team Members</h5>
<div id="membersList"></div>
<button type="button" class="btn btn-outline-primary btn-sm mt-2" onclick="addMember()">
<i class="bi bi-person-plus me-1"></i>Add Team Member
</button>
<div class="alert alert-info alert-permanent d-flex gap-2 mt-3 mb-0" role="alert">
<i class="bi bi-shield-check flex-shrink-0 mt-1"></i>
<div class="small">
<strong>Password requirements:</strong> at least 8 characters with uppercase, lowercase, and a number.
Team members will be able to change their password after logging in.
</div>
</div>
</div>
@await Html.PartialAsync("_WizardFooter", step)
</form>
</div>
</div>
@section Scripts {
<script>
var members = [];
var roleOptions = [
{ value: 'Worker', label: 'Worker (Shop Floor)' },
{ value: 'Manager', label: 'Manager' },
{ value: 'CompanyAdmin', label: 'Company Admin' },
{ value: 'Viewer', label: 'Read-Only Viewer' }
];
function renderMembers() {
var container = document.getElementById('membersList');
if (members.length === 0) {
container.innerHTML = '<p class="text-secondary small py-2">No team members added yet. You can skip this step and add users later.</p>';
} else {
container.innerHTML = members.map(function (m, idx) {
var roleOpts = roleOptions.map(r =>
`<option value="${r.value}"${m.companyRole === r.value ? ' selected' : ''}>${r.label}</option>`
).join('');
return `<div class="wz-item-row">
<div class="row g-2 align-items-end">
<div class="col-md-3">
<label class="form-label small fw-semibold mb-1">First Name</label>
<input class="form-control form-control-sm" value="${escHtml(m.firstName)}" onchange="updateMember(${idx},'firstName',this.value)" placeholder="Jane" />
</div>
<div class="col-md-3">
<label class="form-label small fw-semibold mb-1">Last Name</label>
<input class="form-control form-control-sm" value="${escHtml(m.lastName)}" onchange="updateMember(${idx},'lastName',this.value)" placeholder="Smith" />
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold mb-1">Email <span class="text-danger">*</span></label>
<input class="form-control form-control-sm" type="email" value="${escHtml(m.email)}" onchange="updateMember(${idx},'email',this.value)" placeholder="jane@company.com" />
</div>
<div class="col-md-2 text-end">
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeMember(${idx})" title="Remove">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<div class="row g-2 mt-1">
<div class="col-md-4">
<label class="form-label small fw-semibold mb-1">Password <span class="text-danger">*</span></label>
<input class="form-control form-control-sm" type="password" value="${escHtml(m.password)}" onchange="updateMember(${idx},'password',this.value)" placeholder="Min 8 chars, mixed case + number" />
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold mb-1">Role</label>
<select class="form-select form-select-sm" onchange="updateMember(${idx},'companyRole',this.value)">
${roleOpts}
</select>
</div>
</div>
</div>`;
}).join('');
}
document.getElementById('membersJson').value = JSON.stringify(members);
}
function addMember() {
members.push({ firstName: '', lastName: '', email: '', password: '', companyRole: 'Worker' });
renderMembers();
var inputs = document.querySelectorAll('.wz-item-row:last-child input');
if (inputs.length) inputs[0].focus();
}
function updateMember(idx, field, value) {
members[idx][field] = value;
document.getElementById('membersJson').value = JSON.stringify(members);
}
function removeMember(idx) {
members.splice(idx, 1);
renderMembers();
}
function escHtml(str) {
return (str || '').toString().replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
renderMembers();
</script>
}
@@ -0,0 +1,159 @@
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep2QbDto
@{
ViewData["Title"] = "Setup Wizard — QuickBooks Migration";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 2;
}
@section Styles { @await Html.PartialAsync("_WizardStyles") }
<div class="wizard-layout">
@await Html.PartialAsync("_WizardProgress", progress)
<div class="wizard-content">
<div class="wizard-step-header">
<span class="wizard-step-badge">Step @step of @WizardProgressDto.TotalSteps</span>
<h2><i class="bi bi-arrow-left-right me-2"></i>Migrating from QuickBooks?</h2>
<p class="text-secondary">Let us know so we can guide you through bringing your existing data across.</p>
</div>
<form asp-action="PostStep2" method="post">
@Html.AntiForgeryToken()
<input type="hidden" asp-for="MigratingFromQuickBooks" id="migratingFlag" />
<div class="row g-4 mb-4">
<div class="col-md-6">
<div id="cardYes" onclick="selectMigration(true)"
class="wizard-card h-100 text-center"
style="cursor:pointer; border:2px solid transparent; transition:border-color .15s, background-color .15s;">
<i class="bi bi-cloud-download fs-1 text-primary mb-3 d-block"></i>
<h5 class="fw-bold">Yes, I'm migrating from QuickBooks</h5>
<p class="text-secondary small mb-0">We'll walk you through importing your customers, vendors, items, invoices, and payment history so nothing gets left behind.</p>
</div>
</div>
<div class="col-md-6">
<div id="cardNo" onclick="selectMigration(false)"
class="wizard-card h-100 text-center"
style="cursor:pointer; border:2px solid transparent; transition:border-color .15s, background-color .15s;">
<i class="bi bi-rocket-takeoff fs-1 text-success mb-3 d-block"></i>
<h5 class="fw-bold">No, start fresh</h5>
<p class="text-secondary small mb-0">Start with a clean slate. You can always import data later from the <strong>Tools</strong> page.</p>
</div>
</div>
</div>
@* QB Migration Wizard launcher — shown when "Yes" is selected *@
<div id="qbMigrationSection" class="wizard-card d-none">
<div class="d-flex align-items-start gap-3">
<div class="rounded-3 p-3 d-flex align-items-center justify-content-center flex-shrink-0"
style="background:var(--bs-primary-bg-subtle); color:var(--bs-primary); width:56px; height:56px;">
<i class="bi bi-arrow-left-right fs-4"></i>
</div>
<div class="flex-grow-1">
<h5 class="wizard-card-title mb-1">Which version of QuickBooks are you migrating from?</h5>
<p class="text-secondary mb-3">
Select your QuickBooks version below. Our migration wizard will walk you through all import steps in the correct order with export instructions for each step.
</p>
@* Version picker *@
<div class="row g-3 mb-4" id="qbVersionPicker">
<div class="col-sm-6">
<div id="cardDesktop" onclick="selectQbVersion('desktop')"
class="wizard-card text-center h-100"
style="cursor:pointer; border:2px solid transparent; transition:border-color .15s, background-color .15s;">
<i class="bi bi-pc-display fs-2 text-primary mb-2 d-block"></i>
<h6 class="fw-bold mb-1">QuickBooks Desktop</h6>
<p class="text-secondary small mb-0">Exports IIF &amp; CSV files</p>
</div>
</div>
<div class="col-sm-6">
<div id="cardOnline" onclick="selectQbVersion('online')"
class="wizard-card text-center h-100"
style="cursor:pointer; border:2px solid transparent; transition:border-color .15s, background-color .15s;">
<i class="bi bi-cloud-fill fs-2 text-info mb-2 d-block"></i>
<h6 class="fw-bold mb-1">QuickBooks Online</h6>
<p class="text-secondary small mb-0">Exports Excel (.xlsx) files</p>
</div>
</div>
</div>
@* Completion alert (hidden until wizard is finished) *@
<div id="qbWizardCompletionAlert" class="alert alert-success alert-permanent d-none mb-3">
<i class="bi bi-check-circle-fill me-2"></i>
<strong>Migration complete!</strong> Your data has been imported.
You can reopen the wizard at any time from the <a asp-controller="Tools" asp-action="Index">Tools page</a>.
</div>
<div id="qbLaunchSection" class="d-none">
<button type="button" class="btn btn-primary" id="qbLaunchBtn" onclick="launchSelectedWizard()">
<i class="bi bi-play-fill me-1"></i>Launch Migration Wizard
</button>
<span class="text-secondary small ms-3">
You can also run imports later from <a asp-controller="Tools" asp-action="Index" target="_blank">Tools &rarr; Import &amp; Export</a>.
</span>
</div>
</div>
</div>
</div>
@await Html.PartialAsync("_WizardFooter", step)
</form>
</div>
</div>
@* QB Migration Wizard modal *@
@await Html.PartialAsync("_QbMigrationWizardModal")
@section Scripts {
<script src="~/js/qb-migration-wizard.js" asp-append-version="true"></script>
<script>
var selectedQbVersion = null;
(function () {
var val = document.getElementById('migratingFlag').value;
applyState(val === 'True' || val === 'true');
})();
function selectMigration(isMigrating) {
document.getElementById('migratingFlag').value = isMigrating;
applyState(isMigrating);
}
function applyState(isMigrating) {
var yes = document.getElementById('cardYes');
var no = document.getElementById('cardNo');
var sec = document.getElementById('qbMigrationSection');
yes.style.borderColor = isMigrating ? 'var(--bs-primary)' : 'transparent';
yes.style.backgroundColor = isMigrating ? 'var(--bs-primary-bg-subtle)' : '';
no.style.borderColor = !isMigrating ? 'var(--bs-success)' : 'transparent';
no.style.backgroundColor = !isMigrating ? 'var(--bs-success-bg-subtle)' : '';
sec.classList.toggle('d-none', !isMigrating);
}
function selectQbVersion(version) {
selectedQbVersion = version;
var desktop = document.getElementById('cardDesktop');
var online = document.getElementById('cardOnline');
var launch = document.getElementById('qbLaunchSection');
desktop.style.borderColor = version === 'desktop' ? 'var(--bs-primary)' : 'transparent';
desktop.style.backgroundColor = version === 'desktop' ? 'var(--bs-primary-bg-subtle)' : '';
online.style.borderColor = version === 'online' ? 'var(--bs-info)' : 'transparent';
online.style.backgroundColor = version === 'online' ? 'var(--bs-info-bg-subtle)' : '';
var btn = document.getElementById('qbLaunchBtn');
btn.innerHTML = version === 'online'
? '<i class="bi bi-play-fill me-1"></i>Launch QuickBooks Online Wizard'
: '<i class="bi bi-play-fill me-1"></i>Launch QuickBooks Desktop Wizard';
launch.classList.remove('d-none');
}
function launchSelectedWizard() {
if (selectedQbVersion) {
openQbWizard(selectedQbVersion);
}
}
</script>
}
@@ -0,0 +1,395 @@
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Core.Enums
@model WizardStep2Dto
@{
ViewData["Title"] = "Setup Wizard — Operating Costs";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 3;
}
@section Styles { @await Html.PartialAsync("_WizardStyles") }
<div class="wizard-layout">
@await Html.PartialAsync("_WizardProgress", progress)
<div class="wizard-content">
<div class="wizard-step-header">
<span class="wizard-step-badge">Step @step of @WizardProgressDto.TotalSteps</span>
<h2><i class="bi bi-currency-dollar me-2"></i>Operating Costs</h2>
<p class="text-secondary">These rates drive the automatic pricing engine. Getting them close to your real costs means your quotes will be profitable from day one. Not sure of your numbers? Use the <strong>Help me calculate</strong> buttons next to each section.</p>
</div>
<form asp-action="PostStep3" method="post">
@Html.AntiForgeryToken()
<!-- Shop Capability Tier — quick calibration starting point -->
<div class="wizard-card">
<h5 class="wizard-card-title mb-3"><i class="bi bi-speedometer2 me-2"></i>Shop Size</h5>
<p class="text-secondary small mb-3">
This sets starting defaults for your quoting calibration — how fast your equipment can prep and coat parts.
You can fine-tune this later in <strong>Company Settings → Shop Equipment Profile</strong>.
</p>
<div class="row g-3">
<div class="col-md-8">
<label asp-for="ShopCapabilityTier" class="form-label fw-semibold">What best describes your shop?</label>
<select asp-for="ShopCapabilityTier" class="form-select">
<option value="@((int)ShopCapabilityTier.Garage)">Garage — Home setup, small compressor, part-time</option>
<option value="@((int)ShopCapabilityTier.Small)">Small — 15 person shop</option>
<option value="@((int)ShopCapabilityTier.Medium)">Medium — Established shop, 510 people</option>
<option value="@((int)ShopCapabilityTier.Large)">Large — High-volume operation, 10+ people</option>
</select>
<div class="form-text">This affects how the AI estimates sandblasting and prep time — a garage coater and a large shop blast the same wheel at very different speeds.</div>
</div>
</div>
</div>
<!-- Labor Rates -->
<div class="wizard-card">
<h5 class="wizard-card-title mb-3">Labor Rates <button type="button" class="btn btn-link btn-sm p-0 text-primary fw-normal ms-1" data-bs-toggle="modal" data-bs-target="#laborCalcModal"><i class="bi bi-calculator me-1"></i>Help me calculate</button></h5>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="StandardLaborRate" class="form-label fw-semibold"></label>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="StandardLaborRate" class="form-control" step="0.01" type="number" min="0" />
<span class="input-group-text">/hr</span>
</div>
<div class="form-text">Applied to every hour of hands-on work on a quote item — sandblasting, masking, coating, and general labor time all draw from this rate.</div>
<span asp-validation-for="StandardLaborRate" class="text-danger small"></span>
</div>
</div>
</div>
<!-- Equipment Costs -->
<div class="wizard-card">
<h5 class="wizard-card-title mb-3">Equipment Costs (per hour) <button type="button" class="btn btn-link btn-sm p-0 text-primary fw-normal ms-1" data-bs-toggle="modal" data-bs-target="#equipCalcModal"><i class="bi bi-calculator me-1"></i>Help me calculate</button></h5>
<div class="row g-3">
<div class="col-md-4">
<label asp-for="SandblasterCostPerHour" class="form-label fw-semibold"></label>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="SandblasterCostPerHour" class="form-control" step="0.01" type="number" min="0" />
<span class="input-group-text">/hr</span>
</div>
<div class="form-text">Added to quote items that include sandblasting prep, based on estimated time in the blasting cabinet.</div>
<span asp-validation-for="SandblasterCostPerHour" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="CoatingBoothCostPerHour" class="form-label fw-semibold"></label>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="CoatingBoothCostPerHour" class="form-control" step="0.01" type="number" min="0" />
<span class="input-group-text">/hr</span>
</div>
<div class="form-text">Added to every quote item for time spent in the coating booth applying powder.</div>
<span asp-validation-for="CoatingBoothCostPerHour" class="text-danger small"></span>
</div>
</div>
</div>
<!-- Materials & Pricing -->
<div class="wizard-card">
<h5 class="wizard-card-title mb-3">Materials &amp; Pricing</h5>
<div class="row g-3">
<div class="col-md-4">
<label asp-for="PowderCoatingCostPerSqFt" class="form-label fw-semibold"></label>
<button type="button" class="btn btn-link btn-sm p-0 text-primary fw-normal ms-1" data-bs-toggle="modal" data-bs-target="#powderCalcModal"><i class="bi bi-calculator me-1"></i>Help me calculate</button>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="PowderCoatingCostPerSqFt" class="form-control" step="0.001" type="number" min="0" />
<span class="input-group-text">/sq ft</span>
</div>
<div class="form-text">Your powder material cost per square foot of coated surface. Multiplied by each item's surface area to calculate powder material cost on every quote.</div>
<span asp-validation-for="PowderCoatingCostPerSqFt" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="GeneralMarkupPercentage" class="form-label fw-semibold"></label>
<div class="input-group">
<input asp-for="GeneralMarkupPercentage" class="form-control" step="0.1" type="number" min="0" max="100" />
<span class="input-group-text">%</span>
</div>
<div class="form-text">Applied to the total cost of every quote to add your profit margin. A 30% markup on $100 in costs produces a $130 quote price.</div>
<span asp-validation-for="GeneralMarkupPercentage" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="TaxPercent" class="form-label fw-semibold"></label>
<div class="input-group">
<input asp-for="TaxPercent" class="form-control" step="0.01" type="number" min="0" max="100" />
<span class="input-group-text">%</span>
</div>
<div class="form-text">Sales tax rate applied to the final quote total. Set to 0 if you don't charge sales tax. Tax-exempt customers are automatically excluded.</div>
<span asp-validation-for="TaxPercent" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="ShopMinimumCharge" class="form-label fw-semibold"></label>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="ShopMinimumCharge" class="form-control" step="0.01" type="number" min="0" />
</div>
<div class="form-text">The lowest amount you'll charge for any job, regardless of size. If the calculated quote total falls below this, it's raised to this amount automatically.</div>
<span asp-validation-for="ShopMinimumCharge" class="text-danger small"></span>
</div>
</div>
</div>
@await Html.PartialAsync("_WizardFooter", step)
</form>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════
MODAL 1 — Labor Rate Calculator
═══════════════════════════════════════════════════════════ -->
<div class="modal fade" id="laborCalcModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-calculator me-2"></i>Labor Rate Calculator</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-secondary small mb-3">
Your standard labor rate should cover the wages of everyone working on a job at any given time.
Enter your headcount and average wage to get a starting point.
</p>
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label fw-semibold">Shop employees on floor</label>
<input type="number" id="lc_employees" class="form-control" min="1" value="2" oninput="laborCalc()" />
<div class="form-text">How many people are typically working at once.</div>
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Average hourly wage ($/hr)</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" id="lc_wage" class="form-control" min="0" step="0.01" value="20" oninput="laborCalc()" />
<span class="input-group-text">/hr</span>
</div>
<div class="form-text">Blended average across all shop employees.</div>
</div>
<div class="col-md-4 d-flex align-items-end">
<div class="alert alert-info alert-permanent w-100 py-2 px-3 mb-0">
<div class="small text-secondary">Suggested labor rate</div>
<div class="fs-4 fw-bold" id="lc_result">$40.00/hr</div>
<div class="text-secondary" style="font-size:0.75rem;">employees × wage</div>
</div>
</div>
</div>
<div class="alert alert-warning alert-permanent d-flex gap-2 mb-0" role="alert">
<i class="bi bi-lightbulb flex-shrink-0 mt-1"></i>
<div class="small">
<strong>Tip:</strong> This covers direct labor only. Facility and overhead costs are built into the equipment rates, and your markup percentage adds profit on top. Don't try to roll everything into the labor rate — keep them separate for accurate job costing.
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="applyLaborCalc()">
<i class="bi bi-check2-circle me-1"></i>Apply &amp; Close
</button>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════
MODAL 2 — Equipment Cost Calculator
═══════════════════════════════════════════════════════════ -->
<div class="modal fade" id="equipCalcModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-calculator me-2"></i>Equipment Cost Calculator</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-secondary small mb-3">
Equipment costs are estimated as a percentage of your monthly electric bill. Each piece of equipment draws a known share of total shop electricity. Oven cost is configured per-oven in the next step.
</p>
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label fw-semibold">Monthly electric bill ($)</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" id="ec_elec" class="form-control" min="0" step="1" value="0" oninput="equipCalc()" />
</div>
<div class="form-text">Your total shop electricity bill.</div>
</div>
</div>
<div class="table-responsive mb-3">
<table class="table table-bordered align-middle mb-0" style="font-size:0.88rem;">
<thead class="table-light">
<tr>
<th>Equipment</th>
<th>Basis</th>
<th>Est. Monthly Cost</th>
<th>Suggested Rate</th>
</tr>
</thead>
<tbody>
<tr>
<td><i class="bi bi-wind text-secondary me-1"></i>Spray Booth</td>
<td class="text-secondary small">15% of electric bill</td>
<td><strong id="ec_booth_monthly">$0.00</strong></td>
<td><strong id="ec_booth_rate" class="text-primary">$0.00/hr</strong></td>
</tr>
<tr>
<td><i class="bi bi-robot text-secondary me-1"></i>Sandblast Cabinet + Air Compressor</td>
<td class="text-secondary small">47% of electric bill <span class="text-secondary" style="font-size:0.75rem;">(12% cabinet + 35% compressor)</span></td>
<td><strong id="ec_blast_monthly">$0.00</strong></td>
<td><strong id="ec_blast_rate" class="text-primary">$0.00/hr</strong></td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-warning alert-permanent d-flex gap-2 mb-0" role="alert">
<i class="bi bi-lightbulb flex-shrink-0 mt-1"></i>
<div class="small">
<strong>Tip:</strong> These are estimates based on typical equipment power draw as a share of total shop electricity. Your actual numbers may vary — use these as a starting point and adjust based on your experience.
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="applyEquipCalc()">
<i class="bi bi-check2-circle me-1"></i>Apply &amp; Close
</button>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════
MODAL 3 — Powder Cost Calculator
═══════════════════════════════════════════════════════════ -->
<div class="modal fade" id="powderCalcModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-calculator me-2"></i>Powder Cost Calculator</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-secondary small mb-3">
Powder cost per sq ft is based on what you pay per pound divided by how much area you actually get out of a pound after accounting for overspray and waste.
</p>
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label fw-semibold">Powder price ($/lb)</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" id="pc_price" class="form-control" min="0" step="0.01" value="5.00" oninput="powderCalc()" />
<span class="input-group-text">/lb</span>
</div>
<div class="form-text">Average cost per lb across your typical powder colors.</div>
</div>
<div class="col-md-8">
<label class="form-label fw-semibold">Shop mix / coverage</label>
<select id="pc_coverage" class="form-select" onchange="powderCalc()">
<option value="100">Simple parts, flat panels — ~100 sq ft/lb</option>
<option value="80" selected>Average shop mix — ~80 sq ft/lb</option>
<option value="60">Complex / detailed parts — ~60 sq ft/lb</option>
<option value="45">Heavy rework, lots of masking — ~45 sq ft/lb</option>
</select>
<div class="form-text">Real-world coverage at 23 mil thickness, including overspray &amp; waste.</div>
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<div class="alert alert-info alert-permanent py-3 px-3 mb-0">
<div class="small text-secondary">Suggested powder cost</div>
<div class="fs-3 fw-bold" id="pc_result">$0.063/sq ft</div>
<div class="text-secondary small" id="pc_formula">$5.00 ÷ 80 sq ft/lb</div>
</div>
</div>
<div class="col-md-6 d-flex align-items-center">
<div class="small text-secondary">
<div class="mb-2"><strong>Typical powder prices:</strong></div>
<div>Standard solid colors &nbsp;&nbsp;$36/lb</div>
<div>Metallic / candy &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$610/lb</div>
<div>Specialty / texture &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$815/lb</div>
<div class="mt-2 text-secondary" style="font-size:0.75rem;">Use your blended average if you stock multiple types.</div>
</div>
</div>
</div>
<div class="alert alert-warning alert-permanent d-flex gap-2 mb-0" role="alert">
<i class="bi bi-lightbulb flex-shrink-0 mt-1"></i>
<div class="small">
<strong>Tip:</strong> This is your material cost only — labor and equipment are priced separately. Your markup percentage (set below) adds profit on top of all costs combined.
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="applyPowderCalc()">
<i class="bi bi-check2-circle me-1"></i>Apply &amp; Close
</button>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
// ── Labor Calculator ──────────────────────────────────────
function laborCalc() {
var employees = parseFloat(document.getElementById('lc_employees').value) || 0;
var wage = parseFloat(document.getElementById('lc_wage').value) || 0;
var rate = employees * wage;
document.getElementById('lc_result').textContent = '$' + rate.toFixed(2) + '/hr';
}
function applyLaborCalc() {
var employees = parseFloat(document.getElementById('lc_employees').value) || 0;
var wage = parseFloat(document.getElementById('lc_wage').value) || 0;
document.getElementById('StandardLaborRate').value = (employees * wage).toFixed(2);
bootstrap.Modal.getInstance(document.getElementById('laborCalcModal')).hide();
}
// ── Equipment Calculator ──────────────────────────────────
function equipCalc() {
var elec = parseFloat(document.getElementById('ec_elec').value) || 0;
var boothMonthly = elec * 0.15;
var blastMonthly = elec * 0.47;
document.getElementById('ec_booth_monthly').textContent = '$' + boothMonthly.toFixed(2);
document.getElementById('ec_blast_monthly').textContent = '$' + blastMonthly.toFixed(2);
document.getElementById('ec_booth_rate').textContent = '$' + (boothMonthly / 160).toFixed(2) + '/hr';
document.getElementById('ec_blast_rate').textContent = '$' + (blastMonthly / 160).toFixed(2) + '/hr';
}
function applyEquipCalc() {
var elec = parseFloat(document.getElementById('ec_elec').value) || 0;
document.getElementById('SandblasterCostPerHour').value = ((elec * 0.47) / 160).toFixed(2);
document.getElementById('CoatingBoothCostPerHour').value = ((elec * 0.15) / 160).toFixed(2);
bootstrap.Modal.getInstance(document.getElementById('equipCalcModal')).hide();
}
// ── Powder Calculator ─────────────────────────────────────
function powderCalc() {
var price = parseFloat(document.getElementById('pc_price').value) || 0;
var coverage = parseFloat(document.getElementById('pc_coverage').value) || 80;
var result = coverage > 0 ? price / coverage : 0;
document.getElementById('pc_result').textContent = '$' + result.toFixed(3) + '/sq ft';
document.getElementById('pc_formula').textContent = '$' + price.toFixed(2) + ' ÷ ' + coverage + ' sq ft/lb';
}
function applyPowderCalc() {
var price = parseFloat(document.getElementById('pc_price').value) || 0;
var coverage = parseFloat(document.getElementById('pc_coverage').value) || 80;
document.getElementById('PowderCoatingCostPerSqFt').value = (coverage > 0 ? price / coverage : 0).toFixed(3);
bootstrap.Modal.getInstance(document.getElementById('powderCalcModal')).hide();
}
// ── Init ──────────────────────────────────────────────────
document.getElementById('laborCalcModal').addEventListener('show.bs.modal', laborCalc);
document.getElementById('equipCalcModal').addEventListener('show.bs.modal', equipCalc);
document.getElementById('powderCalcModal').addEventListener('show.bs.modal', powderCalc);
</script>
}
@@ -0,0 +1,355 @@
@using PowderCoating.Application.DTOs.Wizard
@model WizardOvensStepDto
@{
ViewData["Title"] = "Setup Wizard — Shop Equipment";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 4;
}
@section Styles { @await Html.PartialAsync("_WizardStyles") }
<div class="wizard-layout">
@await Html.PartialAsync("_WizardProgress", progress)
<div class="wizard-content">
<div class="wizard-step-header">
<span class="wizard-step-badge">Step @step of @WizardProgressDto.TotalSteps</span>
<h2><i class="bi bi-tools me-2"></i>Shop Equipment</h2>
<p class="text-secondary">Register your ovens and blast setups. Ovens power the Oven Scheduler and quoting engine — at least one is required. Blast setups define each rig's throughput rate so the AI quote engine can estimate sandblasting time accurately.</p>
</div>
<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 ?? "[]")" />
<!-- ── Ovens ─────────────────────────────────────────────────────── -->
<div class="wizard-card">
<h5 class="wizard-card-title"><i class="bi bi-fire me-2"></i>Shop Ovens</h5>
<p class="text-secondary small mb-3">
Each oven appears in the Oven Scheduler's capacity planner and the oven selector on quotes.
Enter dimensions to get a suggested max load, and use the cycle time guide to set a realistic batch time.
</p>
<div id="ovensList"></div>
<button type="button" class="btn btn-outline-primary btn-sm mt-2" onclick="addOven()">
<i class="bi bi-plus-circle me-1"></i>Add Oven
</button>
<div id="ovensValidationMsg" class="alert alert-danger alert-permanent d-flex gap-2 mt-3 mb-0 d-none" role="alert">
<i class="bi bi-exclamation-triangle flex-shrink-0 mt-1"></i>
<div class="small">Please add at least one oven before continuing. You can add more ovens later from <strong>Company Settings &rarr; Shop Ovens</strong>.</div>
</div>
</div>
<!-- ── Blast Setups ──────────────────────────────────────────────── -->
<div class="wizard-card">
<h5 class="wizard-card-title"><i class="bi bi-fan me-2"></i>Blast Setups</h5>
<p class="text-secondary small mb-3">
Add each blast rig in your shop (cabinets, pressure pots, blast rooms). The AI quoting engine uses CFM,
nozzle size, and substrate to derive a sqft/hr throughput rate. Mark one as the default — it will be
pre-selected when quoting. You can also enter a measured rate override if you prefer to use real shop data.
Blast setups are optional; skip this section if you don't do in-house blasting.
</p>
<div id="blastsList"></div>
<button type="button" class="btn btn-outline-primary btn-sm mt-2" onclick="addBlast()">
<i class="bi bi-plus-circle me-1"></i>Add Blast Setup
</button>
</div>
@await Html.PartialAsync("_WizardFooter", step)
</form>
</div>
</div>
@section Scripts {
<script>
// ── Shared helpers ────────────────────────────────────────────────────────
function escHtml(str) {
return (str || '').toString()
.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ═══════════════════════════════════════════════════════════════════════════
// OVENS
// ═══════════════════════════════════════════════════════════════════════════
var ovens = JSON.parse(document.getElementById('ovensJson').value || '[]');
function serializeOvens() {
document.getElementById('ovensJson').value = JSON.stringify(
ovens.map(function(o) {
return { id: o.id || 0, label: o.label, costPerHour: o.costPerHour,
maxLoadSqFt: o.maxLoadSqFt, defaultCycleMinutes: o.defaultCycleMinutes };
})
);
}
function renderOvens() {
var container = document.getElementById('ovensList');
if (ovens.length === 0) {
container.innerHTML = '<p class="text-secondary small py-2">No ovens added yet. Click <strong>Add Oven</strong> to get started.</p>';
} else {
container.innerHTML = ovens.map(function (o, idx) {
return `<div class="wz-item-row mb-2">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label small fw-semibold mb-1">Oven Name <span class="text-danger">*</span></label>
<input class="form-control form-control-sm" value="${escHtml(o.label)}"
onchange="updateOven(${idx},'label',this.value)"
placeholder="e.g. Main Oven, Oven #2" />
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold mb-1">Cost/hr ($) <i class="bi bi-info-circle text-secondary" title="Overrides the default oven rate from Step 3 when this oven is selected on a quote."></i></label>
<input class="form-control form-control-sm" type="number" min="0" step="0.01"
value="${o.costPerHour || 0}"
onchange="updateOvenNum(${idx},'costPerHour',this.value)" />
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold mb-1">Max Load (sq ft)</label>
<input id="maxLoad_${idx}" class="form-control form-control-sm" type="number" min="0" step="0.1"
value="${o.maxLoadSqFt || ''}"
onchange="updateOvenNum(${idx},'maxLoadSqFt',this.value)"
placeholder="e.g. 80" />
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold mb-1">Cycle Time (min)</label>
<input id="cycle_${idx}" class="form-control form-control-sm" type="number" min="1" step="1"
value="${o.defaultCycleMinutes || ''}"
onchange="updateOvenNum(${idx},'defaultCycleMinutes',this.value)"
placeholder="e.g. 50" />
</div>
<div class="col-md-2 text-end">
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeOven(${idx})" title="Remove">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<div class="row g-2 mt-1 align-items-end bg-body-tertiary rounded px-2 py-2">
<div class="col-12 mb-1">
<span class="small fw-semibold text-secondary">
<i class="bi bi-rulers me-1"></i>Dimension Calculator
<span class="fw-normal">(optional — enter interior oven dimensions)</span>
</span>
</div>
<div class="col-md-2">
<label class="form-label small mb-1">Width (ft)</label>
<input class="form-control form-control-sm" type="number" min="0" step="0.1"
id="dimW_${idx}" value="${o._dimW || ''}" oninput="calcDims(${idx})" placeholder="4" />
</div>
<div class="col-md-2">
<label class="form-label small mb-1">Depth (ft)</label>
<input class="form-control form-control-sm" type="number" min="0" step="0.1"
id="dimD_${idx}" value="${o._dimD || ''}" oninput="calcDims(${idx})" placeholder="6" />
</div>
<div class="col-md-2">
<label class="form-label small mb-1">Height (ft)</label>
<input class="form-control form-control-sm" type="number" min="0" step="0.1"
id="dimH_${idx}" value="${o._dimH || ''}" oninput="calcDims(${idx})" placeholder="6" />
</div>
<div class="col-md-3">
<div id="dimSuggestion_${idx}" class="small text-secondary mt-3"></div>
</div>
<div class="col-md-3 small text-secondary">
<div>Typical cycle: 15 min preheat + 20 min cure + 15 min cooldown = <strong>50 min</strong></div>
<button type="button" class="btn btn-link btn-sm p-0 text-primary" onclick="useCycle(${idx})">Use 50 min</button>
</div>
</div>
</div>`;
}).join('');
}
serializeOvens();
}
function addOven() {
ovens.push({ label: '', costPerHour: 0, maxLoadSqFt: null, defaultCycleMinutes: null });
renderOvens();
document.querySelectorAll('#ovensList .wz-item-row:last-child input')[0]?.focus();
}
function removeOven(idx) { ovens.splice(idx, 1); renderOvens(); }
function updateOven(idx, field, value) { ovens[idx][field] = value; serializeOvens(); }
function updateOvenNum(idx, field, value) {
ovens[idx][field] = value === '' ? null : parseFloat(value);
serializeOvens();
}
function calcDims(idx) {
var w = parseFloat(document.getElementById('dimW_' + idx)?.value) || 0;
var d = parseFloat(document.getElementById('dimD_' + idx)?.value) || 0;
var h = parseFloat(document.getElementById('dimH_' + idx)?.value) || 0;
ovens[idx]._dimW = w || null; ovens[idx]._dimD = d || null; ovens[idx]._dimH = h || null;
var el = document.getElementById('dimSuggestion_' + idx);
if (!el) return;
if (w > 0 && h > 0) {
var sqFt = w * h * 0.7;
var volNote = d > 0 ? ` &nbsp;<span class="text-secondary" style="font-size:0.75rem;">(${(w*d*h).toFixed(1)} cu ft)</span>` : '';
el.innerHTML = `Suggested: <strong>${sqFt.toFixed(1)} sq ft</strong>${volNote}<br/>
<span class="text-secondary" style="font-size:0.75rem;">W × H × 70% usable</span><br/>
<button type="button" class="btn btn-link btn-sm p-0 text-primary" onclick="useSqFt(${idx},${sqFt.toFixed(2)})">Use this</button>`;
} else {
el.innerHTML = '<span class="text-secondary" style="font-size:0.75rem;">Enter width &amp; height for a suggestion.</span>';
}
}
function useSqFt(idx, sqFt) {
ovens[idx].maxLoadSqFt = sqFt;
var input = document.getElementById('maxLoad_' + idx);
if (input) input.value = sqFt;
serializeOvens();
}
function useCycle(idx) {
ovens[idx].defaultCycleMinutes = 50;
var input = document.getElementById('cycle_' + idx);
if (input) input.value = 50;
serializeOvens();
}
// ═══════════════════════════════════════════════════════════════════════════
// BLAST SETUPS
// ═══════════════════════════════════════════════════════════════════════════
var blasts = JSON.parse(document.getElementById('blastSetupsJson').value || '[]');
function serializeBlasts() {
document.getElementById('blastSetupsJson').value = JSON.stringify(
blasts.map(function(b) {
return { id: b.id || 0, name: b.name, setupType: b.setupType,
compressorCfm: b.compressorCfm, blastNozzleSize: b.blastNozzleSize,
primarySubstrate: b.primarySubstrate,
blastRateSqFtPerHourOverride: b.blastRateSqFtPerHourOverride,
isDefault: b.isDefault };
})
);
}
function renderBlasts() {
var container = document.getElementById('blastsList');
if (blasts.length === 0) {
container.innerHTML = '<p class="text-secondary small py-2">No blast setups added yet. Click <strong>Add Blast Setup</strong> to get started, or skip if you don\'t do in-house blasting.</p>';
} else {
container.innerHTML = blasts.map(function(b, idx) {
return `<div class="wz-item-row mb-2">
<div class="row g-2 align-items-end">
<div class="col-md-3">
<label class="form-label small fw-semibold mb-1">Setup Name <span class="text-danger">*</span></label>
<input class="form-control form-control-sm" value="${escHtml(b.name)}"
onchange="updateBlast(${idx},'name',this.value)"
placeholder="e.g. Main Cabinet, Blast Room" />
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold mb-1">Type</label>
<select class="form-select form-select-sm" onchange="updateBlastNum(${idx},'setupType',this.value)">
<option value="0" ${b.setupType==0?'selected':''}>Siphon Cabinet</option>
<option value="1" ${b.setupType==1?'selected':''}>Siphon Pot</option>
<option value="2" ${b.setupType==2?'selected':''}>Pressure Pot</option>
<option value="3" ${b.setupType==3?'selected':''}>Wet Blasting</option>
</select>
</div>
<div class="col-md-1">
<label class="form-label small fw-semibold mb-1">CFM</label>
<input class="form-control form-control-sm" type="number" min="0" max="9999" step="0.5"
value="${b.compressorCfm || ''}"
onchange="updateBlastNum(${idx},'compressorCfm',this.value)"
placeholder="40" />
</div>
<div class="col-md-1">
<label class="form-label small fw-semibold mb-1">Nozzle #</label>
<select class="form-select form-select-sm" onchange="updateBlastNum(${idx},'blastNozzleSize',this.value)">
<option value="2" ${b.blastNozzleSize==2?'selected':''}>#2</option>
<option value="3" ${b.blastNozzleSize==3?'selected':''}>#3</option>
<option value="4" ${b.blastNozzleSize==4?'selected':''}>#4</option>
<option value="5" ${b.blastNozzleSize==5?'selected':''}>#5</option>
<option value="6" ${b.blastNozzleSize==6?'selected':''}>#6</option>
<option value="7" ${b.blastNozzleSize==7?'selected':''}>#7</option>
<option value="8" ${b.blastNozzleSize==8?'selected':''}>#8</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold mb-1">Primary Substrate</label>
<select class="form-select form-select-sm" onchange="updateBlastNum(${idx},'primarySubstrate',this.value)">
<option value="0" ${b.primarySubstrate==0?'selected':''}>Paint / light</option>
<option value="3" ${b.primarySubstrate==3?'selected':''}>Mixed (typical)</option>
<option value="2" ${b.primarySubstrate==2?'selected':''}>Rust &amp; scale</option>
<option value="1" ${b.primarySubstrate==1?'selected':''}>Existing powder</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold mb-1">Rate Override <span class="text-secondary fw-normal">(sqft/hr)</span></label>
<input class="form-control form-control-sm" type="number" min="0" step="0.1"
value="${b.blastRateSqFtPerHourOverride != null ? b.blastRateSqFtPerHourOverride : ''}"
onchange="updateBlastOverride(${idx},this.value)"
placeholder="leave blank" />
</div>
<div class="col-md-1 d-flex flex-column align-items-center gap-1">
<label class="form-label small fw-semibold mb-1">Default</label>
<input type="checkbox" class="form-check-input blast-default-cb" ${b.isDefault?'checked':''}
onchange="setBlastDefault(${idx},this.checked)" title="Pre-selected in AI quotes" />
</div>
<div class="col-auto">
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeBlast(${idx})" title="Remove">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>`;
}).join('');
}
serializeBlasts();
}
function addBlast() {
var isFirst = blasts.length === 0;
blasts.push({ name: '', setupType: 2, compressorCfm: '', blastNozzleSize: 5,
primarySubstrate: 3, blastRateSqFtPerHourOverride: null, isDefault: isFirst });
renderBlasts();
document.querySelectorAll('#blastsList .wz-item-row:last-child input')[0]?.focus();
}
function removeBlast(idx) {
blasts.splice(idx, 1);
// If we just removed the default and there are still items, make the first one default
if (blasts.length > 0 && !blasts.some(function(b) { return b.isDefault; })) {
blasts[0].isDefault = true;
}
renderBlasts();
}
function updateBlast(idx, field, value) { blasts[idx][field] = value; serializeBlasts(); }
function updateBlastNum(idx, field, value) {
blasts[idx][field] = value === '' ? null : parseInt(value, 10);
serializeBlasts();
}
function updateBlastOverride(idx, value) {
blasts[idx].blastRateSqFtPerHourOverride = value === '' ? null : parseFloat(value);
serializeBlasts();
}
function setBlastDefault(idx, checked) {
if (checked) {
blasts.forEach(function(b, i) { b.isDefault = i === idx; });
} else {
blasts[idx].isDefault = false;
}
// Re-render to sync all other checkboxes
renderBlasts();
}
// ═══════════════════════════════════════════════════════════════════════════
// VALIDATION + INIT
// ═══════════════════════════════════════════════════════════════════════════
function validateStep4() {
var valid = ovens.filter(function(o) { return o.label && o.label.trim(); }).length > 0;
document.getElementById('ovensValidationMsg').classList.toggle('d-none', valid);
return valid;
}
renderOvens();
renderBlasts();
</script>
}
@@ -0,0 +1,126 @@
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep3Dto
@{
ViewData["Title"] = "Setup Wizard — Document Numbering";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 5;
}
@section Styles { @await Html.PartialAsync("_WizardStyles") }
<div class="wizard-layout">
@await Html.PartialAsync("_WizardProgress", progress)
<div class="wizard-content">
<div class="wizard-step-header">
<span class="wizard-step-badge">Step @step of @WizardProgressDto.TotalSteps</span>
<h2><i class="bi bi-palette me-2"></i>Document Numbering &amp; Branding</h2>
<p class="text-secondary">Customize how your document numbers are formatted and choose the accent color for each PDF type.</p>
</div>
<form asp-action="PostStep5" method="post">
@Html.AntiForgeryToken()
<div class="wizard-card">
<h5 class="wizard-card-title">Document Prefixes</h5>
<p class="text-secondary small mb-3">
Numbers are generated automatically in the format <strong>PREFIX-YYMM-####</strong> — for example, <code>QT-2604-0001</code> or <code>JOB-2604-0001</code>.
You can use any short prefix (up to 10 characters) that makes sense for your business.
</p>
<div class="row g-3">
<div class="col-md-4">
<label asp-for="QuoteNumberPrefix" class="form-label fw-semibold"></label>
<input asp-for="QuoteNumberPrefix" class="form-control" maxlength="10" placeholder="QT" />
<div class="form-text">e.g. <code>QT-2604-0001</code></div>
<span asp-validation-for="QuoteNumberPrefix" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="JobNumberPrefix" class="form-label fw-semibold"></label>
<input asp-for="JobNumberPrefix" class="form-control" maxlength="10" placeholder="JOB" />
<div class="form-text">e.g. <code>JOB-2604-0001</code></div>
<span asp-validation-for="JobNumberPrefix" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="InvoiceNumberPrefix" class="form-label fw-semibold"></label>
<input asp-for="InvoiceNumberPrefix" class="form-control" maxlength="10" placeholder="INV" />
<div class="form-text">e.g. <code>INV-2604-0001</code></div>
<span asp-validation-for="InvoiceNumberPrefix" class="text-danger small"></span>
</div>
</div>
</div>
<div class="wizard-card">
<h5 class="wizard-card-title">PDF Accent Colors</h5>
<p class="text-secondary small mb-3">
Choose the header accent color for each document type. Each can be set independently to match your brand or to visually distinguish document types.
</p>
<div class="row g-4">
<div class="col-md-4">
<label asp-for="QtAccentColor" class="form-label fw-semibold"></label>
<div class="d-flex align-items-center gap-2 mb-2">
<input asp-for="QtAccentColor" class="form-control form-control-color" type="color"
style="width:60px;height:38px;" oninput="syncColor('QtAccentColor','qtHex','qtPreview')" />
<input type="text" id="qtHex" class="form-control" style="width:110px;" maxlength="7"
value="@Model.QtAccentColor" placeholder="#374151"
oninput="syncHex(this,'QtAccentColor','qtPreview')" />
</div>
<div id="qtPreview" class="px-3 py-2 rounded text-white small fw-semibold"
style="background:@Model.QtAccentColor; min-width:140px;">
Quote Header
</div>
<span asp-validation-for="QtAccentColor" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="InAccentColor" class="form-label fw-semibold"></label>
<div class="d-flex align-items-center gap-2 mb-2">
<input asp-for="InAccentColor" class="form-control form-control-color" type="color"
style="width:60px;height:38px;" oninput="syncColor('InAccentColor','inHex','inPreview')" />
<input type="text" id="inHex" class="form-control" style="width:110px;" maxlength="7"
value="@Model.InAccentColor" placeholder="#374151"
oninput="syncHex(this,'InAccentColor','inPreview')" />
</div>
<div id="inPreview" class="px-3 py-2 rounded text-white small fw-semibold"
style="background:@Model.InAccentColor; min-width:140px;">
Invoice Header
</div>
<span asp-validation-for="InAccentColor" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="WoAccentColor" class="form-label fw-semibold"></label>
<div class="d-flex align-items-center gap-2 mb-2">
<input asp-for="WoAccentColor" class="form-control form-control-color" type="color"
style="width:60px;height:38px;" oninput="syncColor('WoAccentColor','woHex','woPreview')" />
<input type="text" id="woHex" class="form-control" style="width:110px;" maxlength="7"
value="@Model.WoAccentColor" placeholder="#374151"
oninput="syncHex(this,'WoAccentColor','woPreview')" />
</div>
<div id="woPreview" class="px-3 py-2 rounded text-white small fw-semibold"
style="background:@Model.WoAccentColor; min-width:140px;">
Work Order Header
</div>
<span asp-validation-for="WoAccentColor" class="text-danger small"></span>
</div>
</div>
</div>
@await Html.PartialAsync("_WizardFooter", step)
</form>
</div>
</div>
@section Scripts {
<script>
function syncColor(inputId, hexId, previewId) {
var val = document.getElementById(inputId).value;
document.getElementById(hexId).value = val;
document.getElementById(previewId).style.background = val;
}
function syncHex(input, colorId, previewId) {
var val = input.value;
if (/^#[0-9A-Fa-f]{6}$/.test(val)) {
document.getElementById(colorId).value = val;
document.getElementById(previewId).style.background = val;
}
}
</script>
}
@@ -0,0 +1,70 @@
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep5Dto
@{
ViewData["Title"] = "Setup Wizard — Job Settings";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 6;
}
@section Styles { @await Html.PartialAsync("_WizardStyles") }
<div class="wizard-layout">
@await Html.PartialAsync("_WizardProgress", progress)
<div class="wizard-content">
<div class="wizard-step-header">
<span class="wizard-step-badge">Step @step of @WizardProgressDto.TotalSteps</span>
<h2><i class="bi bi-diagram-3 me-2"></i>Job &amp; Workflow Settings</h2>
<p class="text-secondary">Configure how jobs flow through your shop and how you interact with customers during the quoting process.</p>
</div>
<div class="alert alert-info alert-permanent d-flex align-items-start gap-2 mb-3" role="alert">
<i class="bi bi-info-circle-fill flex-shrink-0 mt-1"></i>
<div class="small">
<strong>Customer PO Number</strong> — if enabled, a Purchase Order number becomes required before a job can move to active status. This is most useful for commercial/B2B customers who need to tie your invoice to their internal purchasing system. For walk-in retail shops, leave this off.<br />
<strong class="d-block mt-1">Customer Approval via Link</strong> — when enabled, customers receive a secure email link with their quote so they can approve or reject it online without calling you. Approved quotes automatically convert to jobs. This dramatically reduces back-and-forth for remote or commercial clients.
</div>
</div>
<form asp-action="PostStep6" method="post">
@Html.AntiForgeryToken()
<div class="wizard-card">
<h5 class="wizard-card-title">Job Defaults</h5>
<div class="row g-3">
<div class="col-md-5">
<label asp-for="DefaultJobPriority" class="form-label fw-semibold"></label>
<select asp-for="DefaultJobPriority" class="form-select">
<option value="Low">Low</option>
<option value="Normal">Normal</option>
<option value="High">High</option>
<option value="Urgent">Urgent</option>
<option value="Rush">Rush</option>
</select>
</div>
</div>
</div>
<div class="wizard-card">
<h5 class="wizard-card-title">Customer Options</h5>
<div class="row g-3">
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" asp-for="RequireCustomerPO" id="RequireCustomerPO" />
<label class="form-check-label fw-semibold" for="RequireCustomerPO">Require Customer PO Number</label>
</div>
<p class="text-secondary small ms-4 ps-2 mb-0">Jobs require a Purchase Order number before work begins.</p>
</div>
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" asp-for="AllowCustomerApproval" id="AllowCustomerApproval" />
<label class="form-check-label fw-semibold" for="AllowCustomerApproval">Allow Customer Approval via Link</label>
</div>
<p class="text-secondary small ms-4 ps-2 mb-0">Customers receive a link to approve or reject quotes online.</p>
</div>
</div>
</div>
@await Html.PartialAsync("_WizardFooter", step)
</form>
</div>
</div>
@@ -0,0 +1,76 @@
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep4Dto
@{
ViewData["Title"] = "Setup Wizard — Payment Terms";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 7;
}
@section Styles { @await Html.PartialAsync("_WizardStyles") }
<div class="wizard-layout">
@await Html.PartialAsync("_WizardProgress", progress)
<div class="wizard-content">
<div class="wizard-step-header">
<span class="wizard-step-badge">Step @step of @WizardProgressDto.TotalSteps</span>
<h2><i class="bi bi-file-earmark-text me-2"></i>Payment Terms &amp; Quote Defaults</h2>
<p class="text-secondary">Set the default terms that appear on your quotes and invoices. These can always be overridden per document.</p>
</div>
<form asp-action="PostStep7" method="post">
@Html.AntiForgeryToken()
<div class="wizard-card">
<h5 class="wizard-card-title">Payment &amp; Turnaround Defaults</h5>
<div class="row g-3">
<div class="col-md-4">
<label asp-for="DefaultPaymentTerms" class="form-label fw-semibold"></label>
<input asp-for="DefaultPaymentTerms" class="form-control" placeholder="Net 30" />
<div class="form-text">Appears on quotes and invoices (e.g. Net 30, Due on Receipt).</div>
<span asp-validation-for="DefaultPaymentTerms" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="DefaultQuoteValidityDays" class="form-label fw-semibold"></label>
<div class="input-group">
<input asp-for="DefaultQuoteValidityDays" class="form-control" type="number" min="1" max="365" />
<span class="input-group-text">days</span>
</div>
<div class="form-text">How long a quote remains valid before it expires.</div>
<span asp-validation-for="DefaultQuoteValidityDays" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="DefaultTurnaroundDays" class="form-label fw-semibold"></label>
<div class="input-group">
<input asp-for="DefaultTurnaroundDays" class="form-control" type="number" min="1" max="365" />
<span class="input-group-text">days</span>
</div>
<div class="form-text">Default expected completion time for new jobs.</div>
<span asp-validation-for="DefaultTurnaroundDays" class="text-danger small"></span>
</div>
</div>
</div>
<div class="wizard-card">
<h5 class="wizard-card-title">Quote Document Defaults</h5>
<div class="row g-3">
<div class="col-12">
<label asp-for="QtDefaultTerms" class="form-label fw-semibold"></label>
<textarea asp-for="QtDefaultTerms" class="form-control" rows="5"
placeholder="e.g. Payment is due upon completion. Prices valid for 30 days. Customer responsible for prep work unless otherwise noted."></textarea>
<div class="form-text">These terms print in the Terms &amp; Conditions section of every quote PDF.</div>
<span asp-validation-for="QtDefaultTerms" class="text-danger small"></span>
</div>
<div class="col-12">
<label asp-for="QtFooterNote" class="form-label fw-semibold"></label>
<input asp-for="QtFooterNote" class="form-control"
placeholder="e.g. Thank you for your business! Questions? Call us at (555) 123-4567." />
<div class="form-text">A short note that appears at the bottom of quote PDFs.</div>
<span asp-validation-for="QtFooterNote" class="text-danger small"></span>
</div>
</div>
</div>
@await Html.PartialAsync("_WizardFooter", step)
</form>
</div>
</div>
@@ -0,0 +1,117 @@
@using PowderCoating.Application.DTOs.Wizard
@model WizardPricingTiersStepDto
@{
ViewData["Title"] = "Setup Wizard — Pricing Tiers";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 8;
}
@section Styles { @await Html.PartialAsync("_WizardStyles") }
<div class="wizard-layout">
@await Html.PartialAsync("_WizardProgress", progress)
<div class="wizard-content">
<div class="wizard-step-header">
<span class="wizard-step-badge">Step @step of @WizardProgressDto.TotalSteps</span>
<h2><i class="bi bi-percent me-2"></i>Pricing Tiers</h2>
<p class="text-secondary">Create discount tiers for your commercial customers. Once set up, you can assign a tier to any customer so their quotes automatically apply the discount.</p>
</div>
<form asp-action="PostStep8" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="TiersJson" id="tiersJson" value="@(Model.TiersJson ?? "[]")" />
<div class="wizard-card">
<h5 class="wizard-card-title">Pricing Tiers</h5>
<p class="text-secondary small mb-3">
Common examples: <em>Gold (10% off)</em>, <em>Silver (5% off)</em>, <em>Wholesale (15% off)</em>.
Customers without a tier are billed at standard rates.
</p>
<div id="tiersList"></div>
<button type="button" class="btn btn-outline-primary btn-sm mt-2" onclick="addTier()">
<i class="bi bi-plus-circle me-1"></i>Add Pricing Tier
</button>
<div class="alert alert-info alert-permanent d-flex gap-2 mt-3 mb-0" role="alert">
<i class="bi bi-info-circle flex-shrink-0 mt-1"></i>
<div class="small">
You can skip this step and configure pricing tiers later from <strong>Settings &rarr; Pricing Tiers</strong>.
</div>
</div>
</div>
@await Html.PartialAsync("_WizardFooter", step)
</form>
</div>
</div>
@section Scripts {
<script>
var tiers = JSON.parse(document.getElementById('tiersJson').value || '[]');
function renderTiers() {
var container = document.getElementById('tiersList');
if (tiers.length === 0) {
container.innerHTML = '<p class="text-secondary small py-2">No tiers added yet. You can skip this step and set up pricing tiers later from Settings &rarr; Pricing Tiers.</p>';
} else {
container.innerHTML = tiers.map(function (t, idx) {
return `<div class="wz-item-row">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label small fw-semibold mb-1">Tier Name <span class="text-danger">*</span></label>
<input class="form-control form-control-sm" value="${escHtml(t.tierName)}" onchange="updateTier(${idx},'tierName',this.value)" placeholder="e.g. Gold, Wholesale" />
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold mb-1">Description</label>
<input class="form-control form-control-sm" value="${escHtml(t.description)}" onchange="updateTier(${idx},'description',this.value)" placeholder="e.g. High-volume commercial accounts" />
</div>
<div class="col-md-2">
<label class="form-label small fw-semibold mb-1">Discount (%)</label>
<div class="input-group input-group-sm">
<input class="form-control form-control-sm" type="number" min="0" max="100" step="0.1" value="${t.discountPercent || 0}" onchange="updateTierNum(${idx},'discountPercent',this.value)" />
<span class="input-group-text">%</span>
</div>
</div>
<div class="col-md-2 text-end">
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeTier(${idx})" title="Remove">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>`;
}).join('');
}
document.getElementById('tiersJson').value = JSON.stringify(tiers);
}
function addTier() {
tiers.push({ tierName: '', description: '', discountPercent: 0 });
renderTiers();
var inputs = document.querySelectorAll('.wz-item-row:last-child input');
if (inputs.length) inputs[0].focus();
}
function updateTier(idx, field, value) {
tiers[idx][field] = value;
document.getElementById('tiersJson').value = JSON.stringify(tiers);
}
function updateTierNum(idx, field, value) {
tiers[idx][field] = value === '' ? 0 : parseFloat(value);
document.getElementById('tiersJson').value = JSON.stringify(tiers);
}
function removeTier(idx) {
tiers.splice(idx, 1);
renderTiers();
}
function escHtml(str) {
return (str || '').toString().replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
renderTiers();
</script>
}
@@ -0,0 +1,136 @@
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep7Dto
@{
ViewData["Title"] = "Setup Wizard — Notifications";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 9;
}
@section Styles { @await Html.PartialAsync("_WizardStyles") }
<div class="wizard-layout">
@await Html.PartialAsync("_WizardProgress", progress)
<div class="wizard-content">
<div class="wizard-step-header">
<span class="wizard-step-badge">Step @step of @WizardProgressDto.TotalSteps</span>
<h2><i class="bi bi-bell me-2"></i>Notification Preferences</h2>
<p class="text-secondary">Configure which email notifications are sent to your customers when key events happen in the system.</p>
</div>
<form asp-action="PostStep9" method="post">
@Html.AntiForgeryToken()
<div class="wizard-card">
<h5 class="wizard-card-title">Email Sender</h5>
<div class="row g-3">
<div class="col-12">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" asp-for="EmailNotificationsEnabled" id="EmailNotificationsEnabled" />
<label class="form-check-label fw-semibold" for="EmailNotificationsEnabled">Enable Email Notifications</label>
</div>
</div>
<div class="col-md-6">
<label asp-for="EmailFromAddress" class="form-label fw-semibold"></label>
<input asp-for="EmailFromAddress" class="form-control" type="email" placeholder="noreply@yourcompany.com" />
<div class="form-text">Leave blank to use the system default sender.</div>
<span asp-validation-for="EmailFromAddress" class="text-danger small"></span>
</div>
<div class="col-md-6">
<label asp-for="EmailFromName" class="form-label fw-semibold"></label>
<input asp-for="EmailFromName" class="form-control" placeholder="Acme Powder Coating" />
<div class="form-text">The name customers see in their inbox.</div>
</div>
</div>
</div>
<div class="wizard-card">
<h5 class="wizard-card-title">Notification Events</h5>
<p class="text-secondary small mb-3">Choose which events trigger an automatic email to the customer.</p>
<div class="row g-3">
<div class="col-md-6">
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" asp-for="NotifyOnNewJob" id="NotifyOnNewJob" />
<label class="form-check-label" for="NotifyOnNewJob">New Job Created</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" asp-for="NotifyOnNewQuote" id="NotifyOnNewQuote" />
<label class="form-check-label" for="NotifyOnNewQuote">New Quote Sent</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" asp-for="NotifyOnJobStatusChange" id="NotifyOnJobStatusChange" />
<label class="form-check-label" for="NotifyOnJobStatusChange">Job Status Changes</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" asp-for="NotifyOnQuoteApproval" id="NotifyOnQuoteApproval" />
<label class="form-check-label" for="NotifyOnQuoteApproval">Quote Approved</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" asp-for="NotifyOnPaymentReceived" id="NotifyOnPaymentReceived" />
<label class="form-check-label" for="NotifyOnPaymentReceived">Payment Received</label>
</div>
</div>
</div>
</div>
<div class="wizard-card">
<h5 class="wizard-card-title">Automated Payment Reminders</h5>
<p class="text-secondary small mb-3">
Automatically send overdue invoice reminders to customers at configurable intervals.
The system checks daily and sends one reminder per threshold, per invoice.
</p>
<div class="row g-3">
<div class="col-12">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" asp-for="PaymentRemindersEnabled" id="PaymentRemindersEnabled" />
<label class="form-check-label fw-semibold" for="PaymentRemindersEnabled">Enable Automated Payment Reminders</label>
</div>
</div>
<div class="col-md-6">
<label asp-for="PaymentReminderDays" class="form-label fw-semibold"></label>
<input asp-for="PaymentReminderDays" class="form-control" placeholder="7,14,30" />
<div class="form-text">Days past the invoice due date to send a reminder (e.g. <code>7,14,30</code>).</div>
<span asp-validation-for="PaymentReminderDays" class="text-danger small"></span>
</div>
</div>
</div>
<div class="wizard-card">
<h5 class="wizard-card-title">Alert Thresholds</h5>
<p class="text-secondary small mb-3">How far in advance the system warns you about upcoming deadlines and maintenance.</p>
<div class="row g-3">
<div class="col-md-4">
<label asp-for="QuoteExpiryWarningDays" class="form-label fw-semibold"></label>
<div class="input-group">
<input asp-for="QuoteExpiryWarningDays" class="form-control" type="number" min="0" max="90" />
<span class="input-group-text">days</span>
</div>
<div class="form-text">Warn before a quote's expiry date.</div>
<span asp-validation-for="QuoteExpiryWarningDays" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="DueDateWarningDays" class="form-label fw-semibold"></label>
<div class="input-group">
<input asp-for="DueDateWarningDays" class="form-control" type="number" min="0" max="90" />
<span class="input-group-text">days</span>
</div>
<div class="form-text">Warn before a job's due date.</div>
<span asp-validation-for="DueDateWarningDays" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="MaintenanceAlertDays" class="form-label fw-semibold"></label>
<div class="input-group">
<input asp-for="MaintenanceAlertDays" class="form-control" type="number" min="0" max="90" />
<span class="input-group-text">days</span>
</div>
<div class="form-text">Warn before scheduled equipment maintenance.</div>
<span asp-validation-for="MaintenanceAlertDays" class="text-danger small"></span>
</div>
</div>
</div>
@await Html.PartialAsync("_WizardFooter", step)
</form>
</div>
</div>
@@ -0,0 +1,103 @@
@* QuickBooks Migration Wizard — embedded modal partial included in Step2.cshtml *@
<div class="modal fade" id="qbMigrationWizard" tabindex="-1"
aria-labelledby="qbMigrationWizardLabel" aria-hidden="true"
data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<!-- Header -->
<div class="modal-header pb-2">
<div class="d-flex align-items-center gap-2 w-100">
<i class="bi bi-arrow-left-right text-primary fs-5"></i>
<h5 class="modal-title mb-0" id="qbMigrationWizardLabel">QuickBooks Migration Wizard</h5>
<span class="badge bg-primary ms-1" id="qbwStepBadge">Step 1 of 9</span>
<button type="button" class="btn-close ms-auto" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<!-- Step indicator + progress -->
<div class="modal-body border-bottom pb-3">
<div id="qbwStepIndicator" class="d-flex gap-2 flex-wrap justify-content-center mb-3">
<!-- Populated by JS -->
</div>
<div class="progress" style="height:6px;">
<div id="qbwProgressBar" class="progress-bar bg-primary" role="progressbar"
style="width:0%; transition:width .3s ease;"></div>
</div>
</div>
<!-- Dynamic step content -->
<div class="modal-body pt-4" id="qbwStepContent">
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status"></div>
<p class="mt-2 text-secondary">Loading…</p>
</div>
</div>
<!-- Footer -->
<div class="modal-footer border-top-0 pt-0">
<button type="button" class="btn btn-outline-secondary me-auto" id="qbwBtnBack" onclick="qbwBack()">
<i class="bi bi-arrow-left me-1"></i>Back
</button>
<button type="button" class="btn btn-outline-warning" id="qbwBtnSkip" onclick="qbwSkip()">
Skip This Step
</button>
<button type="button" class="btn btn-primary" id="qbwBtnNext" onclick="qbwNext()">
Next Step <i class="bi bi-arrow-right ms-1"></i>
</button>
<button type="button" class="btn btn-success d-none" id="qbwBtnFinish" onclick="qbwFinish()" data-bs-dismiss="modal">
<i class="bi bi-check-lg me-1"></i>Done — Close Wizard
</button>
</div>
</div>
</div>
</div>
@* Inline styles for the step indicator dots *@
<style>
.qbw-dot {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
cursor: pointer;
min-width: 60px;
}
.qbw-dot-circle {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: .8rem;
font-weight: 600;
border: 2px solid currentColor;
transition: transform .15s, box-shadow .15s;
}
.qbw-dot:hover .qbw-dot-circle { transform: scale(1.1); box-shadow: 0 0 0 3px rgba(0,0,0,.08); }
.qbw-dot-label {
font-size: .65rem;
text-align: center;
max-width: 64px;
line-height: 1.2;
color: #6c757d;
}
.qbw-dot.active .qbw-dot-circle { background: var(--bs-primary); color:#fff; border-color:var(--bs-primary); }
.qbw-dot.complete .qbw-dot-circle { background: var(--bs-success); color:#fff; border-color:var(--bs-success); }
.qbw-dot.skipped .qbw-dot-circle { background: var(--bs-warning); color:#000; border-color:var(--bs-warning); }
.qbw-dot.error .qbw-dot-circle { background: var(--bs-danger); color:#fff; border-color:var(--bs-danger); }
.qbw-dot.blocked .qbw-dot-circle { background: #dee2e6; color:#6c757d; border-color:#dee2e6; cursor:not-allowed; }
.qbw-dot.pending .qbw-dot-circle { background: #fff; color:#6c757d; border-color:#dee2e6; }
.qbw-upload-zone {
border: 2px dashed #dee2e6;
border-radius: .5rem;
padding: 1.25rem;
background: #f8f9fa;
transition: border-color .15s, background .15s;
}
.qbw-upload-zone:hover { border-color: var(--bs-primary); background: var(--bs-primary-bg-subtle); }
.qbw-result-box { font-size: .875rem; }
</style>
@@ -0,0 +1,35 @@
@using PowderCoating.Application.DTOs.Wizard
@model int
@{
int step = Model;
bool isLast = step == WizardProgressDto.TotalSteps;
}
<div class="wizard-footer mt-3">
<div>
@if (step > 1)
{
<a asp-action="Step" asp-route-step="@(step - 1)" class="btn btn-outline-secondary me-2">
<i class="bi bi-arrow-left me-1"></i>Back
</a>
}
</div>
<div class="d-flex gap-2">
<form asp-action="Skip" asp-route-step="@step" method="post" class="d-inline">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-link btn-skip p-0 text-secondary">
Skip this step <i class="bi bi-chevron-right"></i>
</button>
</form>
<button type="submit" class="btn btn-primary px-4">
@if (isLast)
{
<span><i class="bi bi-check-circle me-1"></i>Finish Setup</span>
}
else
{
<span>Save &amp; Continue <i class="bi bi-arrow-right ms-1"></i></span>
}
</button>
</div>
</div>
@@ -0,0 +1,68 @@
@using PowderCoating.Application.DTOs.Wizard
@model WizardProgressDto
@{
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"),
};
int currentStep = ViewBag.Step as int? ?? 1;
}
<div class="wizard-sidebar">
<div class="wizard-sidebar-header">
<i class="bi bi-rocket-takeoff me-2"></i>
<span class="fw-semibold">Setup Wizard</span>
<span class="ms-auto badge bg-secondary">@Model.CompletedCount / @WizardProgressDto.TotalSteps</span>
</div>
<div class="wizard-progress-bar">
<div class="wizard-progress-fill" style="width:@Model.ProgressPercent%"></div>
</div>
<nav class="wizard-steps-nav">
@foreach (var (num, label, icon) in steps)
{
var isDone = Model.IsStepDone(num);
var isSkipped = Model.IsStepSkipped(num);
var isCurrent = num == currentStep;
var stateClass = isCurrent ? "current" : isDone ? "done" : isSkipped ? "skipped" : "pending";
<a href="@Url.Action("Step", "SetupWizard", new { step = num })"
class="wizard-step-item @stateClass"
title="@label">
<span class="wizard-step-dot">
@if (isDone)
{
<i class="bi bi-check-lg"></i>
}
else if (isSkipped)
{
<i class="bi bi-dash"></i>
}
else
{
<span>@num</span>
}
</span>
<span class="wizard-step-label">
<i class="@icon me-1"></i>@label
@if (isSkipped && !isDone)
{
<small class="text-warning ms-1">(skipped)</small>
}
</span>
</a>
}
</nav>
</div>
@@ -0,0 +1,198 @@
<style>
/* ── Wizard Layout ─────────────────────────────────── */
.wizard-layout {
display: flex;
gap: 1.5rem;
align-items: flex-start;
min-height: calc(100vh - 120px);
}
/* ── Sidebar ────────────────────────────────────────── */
.wizard-sidebar {
width: 260px;
flex-shrink: 0;
background: var(--bs-body-bg);
border: 1px solid var(--bs-border-color);
border-radius: 0.875rem;
overflow: hidden;
position: sticky;
top: 1.5rem;
}
.wizard-sidebar-header {
display: flex;
align-items: center;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--bs-border-color);
font-size: 0.9rem;
background: var(--bs-tertiary-bg);
}
.wizard-progress-bar {
height: 4px;
background: var(--bs-border-color);
}
.wizard-progress-fill {
height: 100%;
background: linear-gradient(90deg, #6366f1, #8b5cf6);
transition: width 0.4s ease;
}
.wizard-steps-nav {
padding: 0.5rem 0;
}
.wizard-step-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.625rem 1.25rem;
text-decoration: none;
color: var(--bs-secondary-color);
font-size: 0.875rem;
border-left: 3px solid transparent;
transition: all 0.15s;
}
.wizard-step-item:hover {
background: var(--bs-tertiary-bg);
color: var(--bs-emphasis-color);
}
.wizard-step-item.current {
color: #6366f1;
border-left-color: #6366f1;
background: rgba(99, 102, 241, 0.07);
font-weight: 600;
}
.wizard-step-item.done {
color: #059669;
}
.wizard-step-item.skipped {
color: var(--bs-secondary-color);
opacity: 0.75;
}
.wizard-step-dot {
width: 26px;
height: 26px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 700;
flex-shrink: 0;
background: var(--bs-border-color);
color: var(--bs-secondary-color);
}
.wizard-step-item.current .wizard-step-dot {
background: #6366f1;
color: #fff;
}
.wizard-step-item.done .wizard-step-dot {
background: #d1fae5;
color: #059669;
}
.wizard-step-item.skipped .wizard-step-dot {
background: var(--bs-tertiary-bg);
color: var(--bs-secondary-color);
}
.wizard-step-label {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ── Content Area ───────────────────────────────────── */
.wizard-content {
flex: 1;
min-width: 0;
}
.wizard-step-header {
margin-bottom: 1.5rem;
}
.wizard-step-badge {
display: inline-block;
font-size: 0.75rem;
font-weight: 600;
color: #6366f1;
background: rgba(99,102,241,0.1);
padding: 0.25rem 0.75rem;
border-radius: 2rem;
margin-bottom: 0.5rem;
}
.wizard-step-header h2 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.375rem;
}
/* ── Cards ──────────────────────────────────────────── */
.wizard-card {
background: var(--bs-body-bg);
border: 1px solid var(--bs-border-color);
border-radius: 0.875rem;
padding: 1.5rem;
margin-bottom: 1.25rem;
}
.wizard-card-title {
font-size: 0.95rem;
font-weight: 700;
color: var(--bs-emphasis-color);
margin-bottom: 1.25rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--bs-border-color);
}
/* ── Footer ─────────────────────────────────────────── */
.wizard-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 0;
gap: 1rem;
flex-wrap: wrap;
}
.wizard-footer .btn-skip {
font-size: 0.875rem;
color: var(--bs-secondary-color);
}
/* ── Inline list (steps 8 & 9) ─────────────────────── */
.wz-item-row {
background: var(--bs-tertiary-bg);
border: 1px solid var(--bs-border-color);
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 0.75rem;
}
/* ── Color preview swatch ───────────────────────────── */
.color-swatch-preview {
width: 36px;
height: 36px;
border-radius: 0.375rem;
border: 1px solid var(--bs-border-color);
cursor: pointer;
flex-shrink: 0;
}
@@media (max-width: 768px) {
.wizard-layout { flex-direction: column; }
.wizard-sidebar { width: 100%; position: static; }
}
</style>