8aae30765f
Setup Wizard: reduced from 10 steps to 5 (Company Info → QB Migration →
Pricing Defaults → Named Ovens → Notifications). Removed Doc Numbering,
Job Settings, Payment Terms, Pricing Tiers, and Team Members steps — these
all have sensible defaults and are accessible any time in Company Settings.
Wizard now completes in ~5 minutes instead of 15–20.
Dashboard progress widget (new): "Get the most out of your shop" checklist
appears for Company Admins after wizard completion. Tracks six post-setup
activation tasks with dynamic progress badge, motivating subtitle copy,
collapsed-state persistence via localStorage, and a full completion state
("Your shop is fully set up 🎉") that replaces the checklist at 100%.
The next recommended step is highlighted with a solid CTA button and a
subtle blue row tint. Completed steps show encouraging green subtext instead
of just "Done". Widget disappears from controller when AllDone would have
caused a silent vanish — now renders the completion state instead.
Guided activation (Daily Board): rewrote the BoardIntroStep callout to lead
with "This is your shop in real time" and a plain-English description of the
board's purpose. Added a separate InstructionText field to
GuidedActivationCalloutViewModel so the "Move this job to the next stage"
action prompt renders as a distinct bold line with an arrow icon rather than
being buried in the body copy. After the stage change, the confirmation
callout now reads "Nice — your workflow just updated" to reinforce what just
happened before prompting the invoice step.
All copy passes the "shop owner, not SaaS" test: no technical jargon,
benefit-driven descriptions, natural language throughout.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
368 lines
13 KiB
C#
368 lines
13 KiB
C#
using System.ComponentModel.DataAnnotations;
|
||
using PowderCoating.Core.Enums;
|
||
|
||
namespace PowderCoating.Application.DTOs.Wizard;
|
||
|
||
/// <summary>
|
||
/// Tracks the current user's wizard progress (read from CompanyPreferences)
|
||
/// </summary>
|
||
public class WizardProgressDto
|
||
{
|
||
public bool Started { get; set; }
|
||
public bool Completed { get; set; }
|
||
public List<int> DoneSteps { get; set; } = new();
|
||
public List<int> SkippedSteps { get; set; } = new();
|
||
public const int TotalSteps = 5;
|
||
|
||
public bool IsStepDone(int step) => DoneSteps.Contains(step);
|
||
public bool IsStepSkipped(int step) => SkippedSteps.Contains(step);
|
||
public bool IsStepTouched(int step) => IsStepDone(step) || IsStepSkipped(step);
|
||
|
||
public int CompletedCount => DoneSteps.Count + SkippedSteps.Count;
|
||
public int ProgressPercent => TotalSteps == 0 ? 0 : (int)Math.Round((double)CompletedCount / TotalSteps * 100);
|
||
}
|
||
|
||
// ─── Step 1: Company Profile ────────────────────────────────────────────────
|
||
public class WizardStep1Dto
|
||
{
|
||
[Required, MaxLength(200)]
|
||
[Display(Name = "Company Name")]
|
||
public string CompanyName { get; set; } = string.Empty;
|
||
|
||
[MaxLength(200)]
|
||
[Display(Name = "Primary Contact Name")]
|
||
public string? PrimaryContactName { get; set; }
|
||
|
||
[EmailAddress, MaxLength(200)]
|
||
[Display(Name = "Primary Contact Email")]
|
||
public string? PrimaryContactEmail { get; set; }
|
||
|
||
[Phone, MaxLength(50)]
|
||
[Display(Name = "Phone")]
|
||
public string? Phone { get; set; }
|
||
|
||
[MaxLength(300)]
|
||
[Display(Name = "Address")]
|
||
public string? Address { get; set; }
|
||
|
||
[MaxLength(100)]
|
||
[Display(Name = "City")]
|
||
public string? City { get; set; }
|
||
|
||
[MaxLength(100)]
|
||
[Display(Name = "State")]
|
||
public string? State { get; set; }
|
||
|
||
[MaxLength(20)]
|
||
[Display(Name = "ZIP Code")]
|
||
public string? ZipCode { get; set; }
|
||
|
||
[MaxLength(100)]
|
||
[Display(Name = "Time Zone")]
|
||
public string? TimeZone { get; set; }
|
||
|
||
[MaxLength(10)]
|
||
[Display(Name = "Default Currency")]
|
||
public string DefaultCurrency { get; set; } = "USD";
|
||
|
||
[Display(Name = "Use Metric System (metres / kilograms)")]
|
||
public bool UseMetricSystem { get; set; } = false;
|
||
}
|
||
|
||
// ─── Step 2: QuickBooks Migration ───────────────────────────────────────────
|
||
public class WizardStep2QbDto
|
||
{
|
||
public bool MigratingFromQuickBooks { get; set; } = false;
|
||
}
|
||
|
||
// ─── Step 3: Operating Costs (was Step 2) ───────────────────────────────────
|
||
public class WizardStep2Dto
|
||
{
|
||
[Range(0, 10000)]
|
||
[Display(Name = "Standard Labor Rate ($/hr)")]
|
||
public decimal StandardLaborRate { get; set; }
|
||
|
||
[Range(0, 10000)]
|
||
[Display(Name = "Sandblaster Cost ($/hr)")]
|
||
public decimal SandblasterCostPerHour { get; set; }
|
||
|
||
[Range(0, 10000)]
|
||
[Display(Name = "Coating Booth Cost ($/hr)")]
|
||
public decimal CoatingBoothCostPerHour { get; set; }
|
||
|
||
[Range(0, 10000)]
|
||
[Display(Name = "Oven Operating Cost ($/hr)")]
|
||
public decimal OvenOperatingCostPerHour { get; set; }
|
||
|
||
[Range(0, 1000)]
|
||
[Display(Name = "Powder Coating Cost ($/sq ft)")]
|
||
public decimal PowderCoatingCostPerSqFt { get; set; }
|
||
|
||
[Range(0, 100)]
|
||
[Display(Name = "General Markup (%)")]
|
||
public decimal GeneralMarkupPercentage { get; set; }
|
||
|
||
[Range(0, 100)]
|
||
[Display(Name = "Tax Rate (%)")]
|
||
public decimal TaxPercent { get; set; }
|
||
|
||
[Range(0, 100000)]
|
||
[Display(Name = "Shop Minimum Charge ($)")]
|
||
public decimal ShopMinimumCharge { get; set; }
|
||
|
||
[Display(Name = "Shop Capability Tier")]
|
||
public ShopCapabilityTier ShopCapabilityTier { get; set; } = ShopCapabilityTier.Small;
|
||
|
||
// Facility Overhead
|
||
[Range(0, 1000000)]
|
||
[Display(Name = "Monthly Rent ($)")]
|
||
public decimal MonthlyRent { get; set; } = 0m;
|
||
|
||
[Range(0, 1000000)]
|
||
[Display(Name = "Monthly Utilities ($)")]
|
||
public decimal MonthlyUtilities { get; set; } = 0m;
|
||
|
||
[Range(1, 10000)]
|
||
[Display(Name = "Billable Hours/Month")]
|
||
public int MonthlyBillableHours { get; set; } = 160;
|
||
}
|
||
|
||
// ─── Step 3: Branding & Numbering ───────────────────────────────────────────
|
||
public class WizardStep3Dto
|
||
{
|
||
[MaxLength(10)]
|
||
[Display(Name = "Quote Number Prefix")]
|
||
public string QuoteNumberPrefix { get; set; } = "QT";
|
||
|
||
[MaxLength(10)]
|
||
[Display(Name = "Job Number Prefix")]
|
||
public string JobNumberPrefix { get; set; } = "JOB";
|
||
|
||
[MaxLength(10)]
|
||
[Display(Name = "Invoice Number Prefix")]
|
||
public string InvoiceNumberPrefix { get; set; } = "INV";
|
||
|
||
[MaxLength(7)]
|
||
[Display(Name = "Quote Accent Color")]
|
||
public string QtAccentColor { get; set; } = "#374151";
|
||
|
||
[MaxLength(7)]
|
||
[Display(Name = "Invoice Accent Color")]
|
||
public string InAccentColor { get; set; } = "#374151";
|
||
|
||
[MaxLength(7)]
|
||
[Display(Name = "Work Order Accent Color")]
|
||
public string WoAccentColor { get; set; } = "#374151";
|
||
}
|
||
|
||
// ─── Step 4: Invoice & Quote Defaults ───────────────────────────────────────
|
||
public class WizardStep4Dto
|
||
{
|
||
[MaxLength(100)]
|
||
[Display(Name = "Default Payment Terms")]
|
||
public string DefaultPaymentTerms { get; set; } = "Net 30";
|
||
|
||
[Range(1, 365)]
|
||
[Display(Name = "Quote Validity (days)")]
|
||
public int DefaultQuoteValidityDays { get; set; } = 30;
|
||
|
||
[Range(1, 365)]
|
||
[Display(Name = "Default Turnaround (days)")]
|
||
public int DefaultTurnaroundDays { get; set; } = 7;
|
||
|
||
[MaxLength(2000)]
|
||
[Display(Name = "Default Quote Terms & Conditions")]
|
||
public string? QtDefaultTerms { get; set; }
|
||
|
||
[MaxLength(500)]
|
||
[Display(Name = "Quote Footer Note")]
|
||
public string? QtFooterNote { get; set; }
|
||
}
|
||
|
||
// ─── Step 5: Job & Workflow ──────────────────────────────────────────────────
|
||
public class WizardStep5Dto
|
||
{
|
||
[Display(Name = "Default Job Priority")]
|
||
public string DefaultJobPriority { get; set; } = "Normal";
|
||
|
||
[Display(Name = "Require Customer PO Number")]
|
||
public bool RequireCustomerPO { get; set; } = false;
|
||
|
||
[Display(Name = "Allow Customer Approval via Link")]
|
||
public bool AllowCustomerApproval { get; set; } = true;
|
||
|
||
}
|
||
|
||
// ─── Step 6: Chart of Accounts (review only) ────────────────────────────────
|
||
public class WizardStep6Dto
|
||
{
|
||
public int AccountCount { get; set; }
|
||
public int RevenueAccounts { get; set; }
|
||
public int ExpenseAccounts { get; set; }
|
||
public int AssetAccounts { get; set; }
|
||
}
|
||
|
||
// ─── Step 7: Notification Preferences ───────────────────────────────────────
|
||
public class WizardStep7Dto
|
||
{
|
||
[Display(Name = "Enable Email Notifications")]
|
||
public bool EmailNotificationsEnabled { get; set; } = true;
|
||
|
||
[EmailAddress, MaxLength(200)]
|
||
[Display(Name = "Send Emails From Address")]
|
||
public string? EmailFromAddress { get; set; }
|
||
|
||
[MaxLength(200)]
|
||
[Display(Name = "From Display Name")]
|
||
public string? EmailFromName { get; set; }
|
||
|
||
[Display(Name = "Notify on New Job Created")]
|
||
public bool NotifyOnNewJob { get; set; } = true;
|
||
|
||
[Display(Name = "Notify on New Quote Created")]
|
||
public bool NotifyOnNewQuote { get; set; } = true;
|
||
|
||
[Display(Name = "Notify on Job Status Change")]
|
||
public bool NotifyOnJobStatusChange { get; set; } = true;
|
||
|
||
[Display(Name = "Notify on Quote Approval")]
|
||
public bool NotifyOnQuoteApproval { get; set; } = true;
|
||
|
||
[Display(Name = "Notify on Payment Received")]
|
||
public bool NotifyOnPaymentReceived { get; set; } = true;
|
||
|
||
[Display(Name = "Enable Automated Payment Reminders")]
|
||
public bool PaymentRemindersEnabled { get; set; } = false;
|
||
|
||
[MaxLength(50)]
|
||
[Display(Name = "Reminder Days (comma-separated, days past due)")]
|
||
public string PaymentReminderDays { get; set; } = "7,14,30";
|
||
|
||
// Alert Thresholds
|
||
[Range(0, 90)]
|
||
[Display(Name = "Quote Expiry Warning (days before expiry)")]
|
||
public int QuoteExpiryWarningDays { get; set; } = 3;
|
||
|
||
[Range(0, 90)]
|
||
[Display(Name = "Due Date Warning (days before due)")]
|
||
public int DueDateWarningDays { get; set; } = 2;
|
||
|
||
[Range(0, 90)]
|
||
[Display(Name = "Maintenance Alert (days before scheduled date)")]
|
||
public int MaintenanceAlertDays { get; set; } = 7;
|
||
}
|
||
|
||
// ─── Step 8: Inventory / Powder Colors ──────────────────────────────────────
|
||
public class WizardStep8Dto
|
||
{
|
||
/// <summary>JSON array of WizardInventoryItemDto submitted as a hidden field</summary>
|
||
public string? ItemsJson { get; set; }
|
||
}
|
||
|
||
public class WizardInventoryItemDto
|
||
{
|
||
public string Name { get; set; } = string.Empty;
|
||
public string? ColorName { get; set; }
|
||
public string? ColorCode { get; set; }
|
||
public string? Manufacturer { get; set; }
|
||
public string? Finish { get; set; }
|
||
public decimal UnitCost { get; set; }
|
||
public decimal QuantityOnHand { get; set; }
|
||
}
|
||
|
||
// ─── Step 9: Team Members ────────────────────────────────────────────────────
|
||
public class WizardStep9Dto
|
||
{
|
||
/// <summary>JSON array of WizardTeamMemberDto submitted as a hidden field</summary>
|
||
public string? MembersJson { get; set; }
|
||
}
|
||
|
||
public class WizardTeamMemberDto
|
||
{
|
||
public string FirstName { get; set; } = string.Empty;
|
||
public string LastName { get; set; } = string.Empty;
|
||
public string Email { get; set; } = string.Empty;
|
||
public string Password { get; set; } = string.Empty;
|
||
public string CompanyRole { get; set; } = "Worker";
|
||
}
|
||
|
||
// ─── Step 10: Vendors & Suppliers ────────────────────────────────────────────
|
||
public class WizardStep10Dto
|
||
{
|
||
/// <summary>JSON array of WizardVendorDto submitted as a hidden field</summary>
|
||
public string? VendorsJson { get; set; }
|
||
}
|
||
|
||
public class WizardVendorDto
|
||
{
|
||
public string CompanyName { get; set; } = string.Empty;
|
||
public string? ContactName { get; set; }
|
||
public string? Email { get; set; }
|
||
public string? Phone { get; set; }
|
||
public string? Website { get; set; }
|
||
}
|
||
|
||
// ─── Step 14: Equipment / Named Ovens + Blast Setups ────────────────────────
|
||
public class WizardOvensStepDto
|
||
{
|
||
/// <summary>JSON array of WizardOvenDto submitted as a hidden field</summary>
|
||
public string? OvensJson { get; set; }
|
||
|
||
/// <summary>JSON array of WizardBlastSetupDto submitted as a hidden field</summary>
|
||
public string? BlastSetupsJson { get; set; }
|
||
}
|
||
|
||
public class WizardOvenDto
|
||
{
|
||
public int Id { get; set; }
|
||
public string Label { get; set; } = string.Empty;
|
||
public decimal CostPerHour { get; set; }
|
||
public decimal? MaxLoadSqFt { get; set; }
|
||
public int? DefaultCycleMinutes { get; set; }
|
||
}
|
||
|
||
public class WizardBlastSetupDto
|
||
{
|
||
public int Id { get; set; }
|
||
public string Name { get; set; } = string.Empty;
|
||
/// <summary>Maps to BlastSetupType enum: 0=SiphonCabinet, 1=SiphonPot, 2=PressurePot, 3=WetBlasting</summary>
|
||
public int SetupType { get; set; } = 2;
|
||
public decimal CompressorCfm { get; set; }
|
||
/// <summary>Nozzle orifice number 2–8.</summary>
|
||
public int BlastNozzleSize { get; set; } = 5;
|
||
/// <summary>Maps to BlastSubstrateType enum: 0=Paint, 1=PowderCoat, 2=RustAndScale, 3=Mixed</summary>
|
||
public int PrimarySubstrate { get; set; } = 3;
|
||
public decimal? BlastRateSqFtPerHourOverride { get; set; }
|
||
public bool IsDefault { get; set; }
|
||
}
|
||
|
||
// ─── Step 15: Pricing Tiers ──────────────────────────────────────────────────
|
||
public class WizardPricingTiersStepDto
|
||
{
|
||
/// <summary>JSON array of WizardPricingTierDto submitted as a hidden field</summary>
|
||
public string? TiersJson { get; set; }
|
||
}
|
||
|
||
public class WizardPricingTierDto
|
||
{
|
||
public int Id { get; set; }
|
||
public string TierName { get; set; } = string.Empty;
|
||
public string? Description { get; set; }
|
||
public decimal DiscountPercent { get; set; }
|
||
}
|
||
|
||
// ─── Step 16: Service Catalog ────────────────────────────────────────────────
|
||
public class WizardCatalogStepDto
|
||
{
|
||
/// <summary>JSON array of WizardCatalogItemDto submitted as a hidden field</summary>
|
||
public string? ItemsJson { get; set; }
|
||
}
|
||
|
||
public class WizardCatalogItemDto
|
||
{
|
||
public string Name { get; set; } = string.Empty;
|
||
public string? Description { get; set; }
|
||
public decimal DefaultPrice { get; set; }
|
||
public decimal? ApproximateArea { get; set; }
|
||
}
|