Files
PowderCoatingLogix/src/PowderCoating.Application/DTOs/Wizard/WizardDtos.cs
T
spouliot 8aae30765f Onboarding overhaul: slim wizard, progress widget, guided activation UX
Setup Wizard: reduced from 10 steps to 5 (Company Info → QB Migration →
Pricing Defaults → Named Ovens → Notifications). Removed Doc Numbering,
Job Settings, Payment Terms, Pricing Tiers, and Team Members steps — these
all have sensible defaults and are accessible any time in Company Settings.
Wizard now completes in ~5 minutes instead of 15–20.

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

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 21:10:47 -04:00

368 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 28.</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; }
}