Files
PowderCoatingLogix/src/PowderCoating.Application/DTOs/Wizard/WizardDtos.cs
T
spouliot ed35362c7a Add Formula Library ratings, Job Profitability report, and Quote Revision History improvements
- Formula Library ratings: thumbs up/down per company per formula; toggle on/off; sorts by net score; own formulas not rateable; FormulaLibraryRating entity + migration AddFormulaLibraryRatings
- Job Profitability report: actual labor cost (logged hours x StandardLaborRate) vs powder cost vs billed price per job; gross margin % color-coded; time-tracked-only filter; totals footer
- Quote Revision History: track Total price changes on every save; log Sent/Resent events with recipient email; replace flat table with grouped timeline UI (icons per event type, total-change badge on header)
- Setup Wizard: cap CompletedCount at TotalSteps so old 10-step data no longer shows 10/5
- Formula Library card: fix badge overflow on long titles; add Rate: label to make voting buttons discoverable
- Help docs and AI knowledge base updated for all three features

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 09:02:07 -04:00

369 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);
// Capped at TotalSteps so old step data from a larger wizard doesn't overflow the display.
public int CompletedCount => Math.Min(DoneSteps.Count + SkippedSteps.Count, TotalSteps);
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; }
}