Files
PowderCoatingLogix/src/PowderCoating.Application/DTOs/Job/JobDtos.cs
T
spouliot 1eba50cf0f Add Custom Formula Item Templates with AI generation and wizard integration
Introduces per-company reusable NCalc2 pricing formula templates for complex
fabricated items (roof curbs, enclosures, welded frames). Templates support
two output modes — FixedRate (formula yields a dollar amount) and SurfaceAreaSqFt
(formula yields sq ft fed into the standard coating engine). Includes:

- CustomItemTemplate entity, migration (AddCustomItemTemplates), IUnitOfWork repo
- IsCustomFormulaItem / CustomItemTemplateId / FormulaFieldValuesJson flags on
  QuoteItem, JobItem, CreateQuoteItemDto; mapped in all 3 JobItemAssemblyService
  overloads and all existingItemsData JSON projections + pageMeta blocks
- ICustomFormulaAiService / CustomFormulaAiService: Claude-powered formula
  generator (natural language + optional diagram image) and NCalc2 evaluator
- CompanySettings CRUD endpoints: GetCustomItemTemplates, Create/Update/Delete,
  UploadTemplateDiagram, TemplateDiagram (blob serve), EvaluateFormula, GenerateFormulaFromAi
- Company Settings "Custom Formulas" tab + cfModal + company-settings-custom-formulas.js
- item-wizard.js: formula item type card, renderFormulaFields, wzFormulaRecalc
  (live evaluate via POST), collectStep2 formula branch, buildCardHtml / emitHiddenFields
- Formula badge in Quotes/Details and Jobs/Details; AI badge gap fixed in Jobs/Details
- Help article (CustomFormulaTemplates.cshtml), Help Index card, HelpController action,
  HelpKnowledgeBase entry; 225/225 unit tests passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 15:09:22 -04:00

569 lines
21 KiB
C#

using System.ComponentModel.DataAnnotations;
using PowderCoating.Application.DTOs.PrepService;
using PowderCoating.Application.DTOs.Quote;
namespace PowderCoating.Application.DTOs.Job;
public class JobDto
{
public int Id { get; set; }
public string JobNumber { get; set; } = string.Empty;
public int CustomerId { get; set; }
public string CustomerName { get; set; } = string.Empty;
public string? CustomerCompanyName { get; set; }
public string? CustomerContactName { get; set; }
public int? QuoteId { get; set; }
public string? QuoteNumber { get; set; }
public string? AssignedUserId { get; set; }
public string? AssignedWorkerName { get; set; }
public string Description { get; set; } = string.Empty;
// Job Status (from lookup table)
public int JobStatusId { get; set; }
public string StatusCode { get; set; } = string.Empty; // For code logic
public string StatusDisplayName { get; set; } = string.Empty; // For UI
public string StatusColorClass { get; set; } = "secondary"; // For badges
public string? StatusIconClass { get; set; } // For icons
public int StatusDisplayOrder { get; set; } // For comparisons
public bool StatusIsTerminal { get; set; } // For filtering
public bool StatusIsWIP { get; set; } // For stats
// Job Priority (from lookup table)
public int JobPriorityId { get; set; }
public string PriorityCode { get; set; } = string.Empty;
public string PriorityDisplayName { get; set; } = string.Empty;
public string PriorityColorClass { get; set; } = "secondary";
public string? PriorityIconClass { get; set; }
public int PriorityDisplayOrder { get; set; }
public DateTime? ScheduledDate { get; set; }
public DateTime? DueDate { get; set; }
public DateTime? CompletedDate { get; set; }
// Oven selection (carried over from quote)
public int? OvenCostId { get; set; }
public string? OvenLabel { get; set; }
public decimal QuotedPrice { get; set; }
public decimal FinalPrice { get; set; }
public decimal ShopSuppliesAmount { get; set; }
public decimal ShopSuppliesPercent { get; set; }
public bool IsRushJob { get; set; }
public string DiscountType { get; set; } = "None";
public decimal DiscountValue { get; set; }
public string? DiscountReason { get; set; }
public string? CustomerPO { get; set; }
public string? SpecialInstructions { get; set; }
public string? InternalNotes { get; set; }
public string? Tags { get; set; }
public bool RequiresCustomerApproval { get; set; }
public bool IsCustomerApproved { get; set; }
public string? CustomerEmail { get; set; }
public bool CustomerNotifyByEmail { get; set; } = true;
// Customer SMS opt-in — used for SMS compose modal on job details
public bool CustomerNotifyBySms { get; set; }
public string? CustomerMobilePhone { get; set; }
// Job Completion Details
public decimal? ActualTimeSpentHours { get; set; }
// Part intake / receiving
public DateTime? IntakeDate { get; set; }
public string? IntakeConditionNotes { get; set; }
public int? IntakePartCount { get; set; }
public string? IntakeCheckedByUserId { get; set; }
public string? IntakeCheckedByName { get; set; }
// Time tracking
public List<JobTimeEntryDto> TimeEntries { get; set; } = new();
public decimal TotalLoggedHours => TimeEntries.Sum(t => t.HoursWorked);
// Rework
public bool IsReworkJob { get; set; }
public int? OriginalJobId { get; set; }
public string? OriginalJobNumber { get; set; }
public List<JobItemDto> Items { get; set; } = new();
public List<PrepServiceDto> PrepServices { get; set; } = new();
public List<int> PrepServiceIds { get; set; } = new();
public DateTime CreatedAt { get; set; }
}
public class JobListDto
{
public int Id { get; set; }
public string JobNumber { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public string? AssignedUserId { get; set; }
public string? AssignedWorkerName { get; set; }
// Job Status (from lookup table)
public int JobStatusId { get; set; }
public string StatusCode { get; set; } = string.Empty;
public string StatusDisplayName { get; set; } = string.Empty;
public string StatusColorClass { get; set; } = "secondary";
public bool StatusIsWIP { get; set; }
// Job Priority (from lookup table)
public int JobPriorityId { get; set; }
public string PriorityCode { get; set; } = string.Empty;
public string PriorityDisplayName { get; set; } = string.Empty;
public string PriorityColorClass { get; set; } = "secondary";
public string? CustomerEmail { get; set; }
public bool CustomerNotifyByEmail { get; set; } = true;
public DateTime? ScheduledDate { get; set; }
public DateTime? DueDate { get; set; }
public decimal FinalPrice { get; set; }
public DateTime CreatedAt { get; set; }
public string? Tags { get; set; }
public bool IsReworkJob { get; set; }
public int? OriginalJobId { get; set; }
}
public class CreateJobDto
{
[Required(ErrorMessage = "Customer is required")]
[Display(Name = "Customer")]
public int CustomerId { get; set; }
[Display(Name = "Quote")]
public int? QuoteId { get; set; }
[Display(Name = "Assigned Worker")]
public string? AssignedUserId { get; set; }
[Display(Name = "Oven")]
public int? OvenCostId { get; set; }
[Display(Name = "Batches")]
[Range(1, 999)]
public int OvenBatches { get; set; } = 1;
[Display(Name = "Cycle Time (min)")]
public int? OvenCycleMinutes { get; set; }
[Required(ErrorMessage = "Description is required")]
[StringLength(2000, ErrorMessage = "Description cannot exceed 2000 characters")]
[Display(Name = "Description")]
public string Description { get; set; } = string.Empty;
[Required(ErrorMessage = "Priority is required")]
[Display(Name = "Priority")]
public int JobPriorityId { get; set; } // FK to lookup table
[Display(Name = "Scheduled Date")]
public DateTime? ScheduledDate { get; set; }
[Display(Name = "Due Date")]
public DateTime? DueDate { get; set; }
[Range(0, 9999999.99, ErrorMessage = "Quoted price must be between 0 and 9,999,999.99")]
[Display(Name = "Quoted Price")]
public decimal QuotedPrice { get; set; }
[StringLength(100, ErrorMessage = "Customer PO cannot exceed 100 characters")]
[Display(Name = "Customer PO")]
public string? CustomerPO { get; set; }
[StringLength(2000, ErrorMessage = "Special instructions cannot exceed 2000 characters")]
[Display(Name = "Special Instructions")]
public string? SpecialInstructions { get; set; }
[StringLength(2000, ErrorMessage = "Internal notes cannot exceed 2000 characters")]
[Display(Name = "Internal Notes")]
public string? InternalNotes { get; set; }
[Display(Name = "Tags")]
[StringLength(500)]
public string? Tags { get; set; }
[Display(Name = "Requires Customer Approval")]
public bool RequiresCustomerApproval { get; set; }
[Display(Name = "Rush Job")]
public bool IsRushJob { get; set; }
[Display(Name = "Discount Type")]
public string DiscountType { get; set; } = "None";
[Range(0, double.MaxValue, ErrorMessage = "Discount value must be 0 or greater")]
[Display(Name = "Discount Value")]
public decimal DiscountValue { get; set; }
[StringLength(500)]
[Display(Name = "Discount Reason")]
public string? DiscountReason { get; set; }
public List<CreateQuoteItemDto> JobItems { get; set; } = new();
public List<int> PrepServiceIds { get; set; } = new();
}
public class UpdateJobDto
{
[Required]
public int Id { get; set; }
[Required(ErrorMessage = "Customer is required")]
[Display(Name = "Customer")]
public int CustomerId { get; set; }
[Display(Name = "Quote")]
public int? QuoteId { get; set; }
[Display(Name = "Assigned Worker")]
public string? AssignedUserId { get; set; }
[Display(Name = "Oven")]
public int? OvenCostId { get; set; }
[Display(Name = "Batches")]
[Range(1, 999)]
public int OvenBatches { get; set; } = 1;
[Display(Name = "Cycle Time (min)")]
public int? OvenCycleMinutes { get; set; }
[Required(ErrorMessage = "Description is required")]
[StringLength(2000, ErrorMessage = "Description cannot exceed 2000 characters")]
[Display(Name = "Description")]
public string Description { get; set; } = string.Empty;
[Required(ErrorMessage = "Status is required")]
[Display(Name = "Status")]
public int JobStatusId { get; set; } // FK to lookup table
[Required(ErrorMessage = "Priority is required")]
[Display(Name = "Priority")]
public int JobPriorityId { get; set; } // FK to lookup table
[Display(Name = "Scheduled Date")]
public DateTime? ScheduledDate { get; set; }
[Display(Name = "Due Date")]
public DateTime? DueDate { get; set; }
[Range(0, 9999999.99, ErrorMessage = "Quoted price must be between 0 and 9,999,999.99")]
[Display(Name = "Quoted Price")]
public decimal QuotedPrice { get; set; }
[StringLength(100, ErrorMessage = "Customer PO cannot exceed 100 characters")]
[Display(Name = "Customer PO")]
public string? CustomerPO { get; set; }
[StringLength(2000, ErrorMessage = "Special instructions cannot exceed 2000 characters")]
[Display(Name = "Special Instructions")]
public string? SpecialInstructions { get; set; }
[StringLength(2000, ErrorMessage = "Internal notes cannot exceed 2000 characters")]
[Display(Name = "Internal Notes")]
public string? InternalNotes { get; set; }
[Display(Name = "Tags")]
[StringLength(500)]
public string? Tags { get; set; }
[Display(Name = "Requires Customer Approval")]
public bool RequiresCustomerApproval { get; set; }
[Display(Name = "Rush Job")]
public bool IsRushJob { get; set; }
[Display(Name = "Discount Type")]
public string DiscountType { get; set; } = "None";
[Range(0, double.MaxValue, ErrorMessage = "Discount value must be 0 or greater")]
[Display(Name = "Discount Value")]
public decimal DiscountValue { get; set; }
[StringLength(500)]
[Display(Name = "Discount Reason")]
public string? DiscountReason { get; set; }
public List<CreateQuoteItemDto> JobItems { get; set; } = new();
public List<int> PrepServiceIds { get; set; } = new();
[Display(Name = "Notify customer of status change via email")]
public bool SendEmailOnStatusChange { get; set; } = false;
}
public class UpdateJobItemDto
{
public int Id { get; set; }
public string Description { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public string? ColorName { get; set; }
public string? ColorCode { get; set; }
public decimal? SurfaceArea { get; set; }
public decimal UnitPrice { get; set; }
public decimal TotalPrice { get; set; }
public bool RequiresSandblasting { get; set; }
public bool RequiresMasking { get; set; }
public int EstimatedMinutes { get; set; }
public string? Notes { get; set; }
}
public class JobItemDto
{
public int Id { get; set; }
public string Description { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public string? ColorName { get; set; }
public string? ColorCode { get; set; }
public string? Finish { get; set; }
public decimal? SurfaceArea { get; set; }
public decimal SurfaceAreaSqFt { get; set; }
public int EstimatedMinutes { get; set; }
public decimal UnitPrice { get; set; }
public decimal TotalPrice { get; set; }
public decimal LaborCost { get; set; }
public bool RequiresSandblasting { get; set; }
public bool RequiresMasking { get; set; }
public string? Notes { get; set; }
public int? CatalogItemId { get; set; }
public bool IsGenericItem { get; set; }
public bool IsLaborItem { get; set; }
public bool IsSalesItem { get; set; }
public bool IsAiItem { get; set; }
public string? Sku { get; set; }
public bool IsCustomFormulaItem { get; set; }
public int? CustomItemTemplateId { get; set; }
public string? FormulaFieldValuesJson { get; set; }
public List<JobItemCoatDto> Coats { get; set; } = new();
public List<JobItemPrepServiceDto> PrepServices { get; set; } = new();
}
public class JobItemPrepServiceDto
{
public int PrepServiceId { get; set; }
public string? PrepServiceName { get; set; }
public int EstimatedMinutes { get; set; }
/// <summary>Blast setup selected in wizard for this sandblasting prep service.</summary>
public int? BlastSetupId { get; set; }
}
public class CreateJobItemDto
{
public string Description { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public string? ColorName { get; set; }
public string? ColorCode { get; set; }
public string? Finish { get; set; }
public decimal UnitPrice { get; set; }
public bool RequiresSandblasting { get; set; }
public bool RequiresMasking { get; set; }
public int EstimatedMinutes { get; set; }
public string? Notes { get; set; }
}
// DTO for Shop Floor Display
public class ShopFloorJobDto
{
public int Id { get; set; }
public string JobNumber { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
// Job Status (from lookup table)
public int JobStatusId { get; set; }
public string StatusCode { get; set; } = string.Empty;
public string StatusDisplayName { get; set; } = string.Empty;
public string StatusColorClass { get; set; } = "secondary";
// Job Priority (from lookup table)
public int JobPriorityId { get; set; }
public string PriorityCode { get; set; } = string.Empty;
public string PriorityDisplayName { get; set; } = string.Empty;
public string PriorityColorClass { get; set; } = "secondary";
public string? AssignedWorkerName { get; set; }
public DateTime? ScheduledDate { get; set; }
public DateTime? DueDate { get; set; }
public int ItemCount { get; set; }
public List<string> NextSteps { get; set; } = new();
}
// DTO for Job Item Coat (multi-coat support)
public class JobItemCoatDto
{
public int Id { get; set; }
public int JobItemId { get; set; }
public string CoatName { get; set; } = string.Empty;
public int Sequence { get; set; }
public int? InventoryItemId { get; set; }
public string? ColorName { get; set; }
public int? VendorId { get; set; }
public string? VendorName { get; set; }
public string? ColorCode { get; set; }
public string? Finish { get; set; }
public decimal CoverageSqFtPerLb { get; set; }
public decimal TransferEfficiency { get; set; }
public decimal? PowderCostPerLb { get; set; }
public decimal? PowderToOrder { get; set; }
public decimal? ActualPowderUsedLbs { get; set; } // Filled during job completion
public bool NoExtraLayerCharge { get; set; }
public string? Notes { get; set; }
}
// DTO for completing a job
public class CompleteJobDto
{
public int JobId { get; set; }
public decimal? ActualTimeSpentHours { get; set; }
public List<JobPowderUsageDto> PowderUsages { get; set; } = new();
public bool SendEmailToCustomer { get; set; } = false;
}
// DTO for the Admin/Manager compose-before-send SMS endpoint
public class SendJobSmsRequest
{
public int JobId { get; set; }
public string Message { get; set; } = string.Empty;
}
// DTO for tracking actual powder usage per inventory item (color) for the whole job
public class JobPowderUsageDto
{
public int InventoryItemId { get; set; }
public decimal? ActualPowderUsedLbs { get; set; }
}
// ── Time Tracking DTOs ────────────────────────────────────────────────────────
public class JobTimeEntryDto
{
public int Id { get; set; }
public int JobId { get; set; }
public string? UserId { get; set; }
public string WorkerName { get; set; } = string.Empty;
public DateTime WorkDate { get; set; }
public decimal HoursWorked { get; set; }
public string? Stage { get; set; }
public string? Notes { get; set; }
public DateTime CreatedAt { get; set; }
}
public class CreateJobTimeEntryDto
{
public int JobId { get; set; }
public string UserId { get; set; } = string.Empty;
public DateTime WorkDate { get; set; }
public decimal HoursWorked { get; set; }
public string? Stage { get; set; }
public string? Notes { get; set; }
}
public class UpdateJobTimeEntryDto
{
public int Id { get; set; }
public string UserId { get; set; } = string.Empty;
public DateTime WorkDate { get; set; }
public decimal HoursWorked { get; set; }
public string? Stage { get; set; }
public string? Notes { get; set; }
}
// ── Rework / Warranty DTOs ────────────────────────────────────────────────────
public class ReworkRecordDto
{
public int Id { get; set; }
public int JobId { get; set; }
public int? JobItemId { get; set; }
public string? JobItemDescription { get; set; }
public int? ReworkJobId { get; set; }
public string? ReworkJobNumber { get; set; }
public PowderCoating.Core.Enums.ReworkType ReworkType { get; set; }
public string ReworkTypeDisplay { get; set; } = string.Empty;
public PowderCoating.Core.Enums.ReworkReason Reason { get; set; }
public string ReasonDisplay { get; set; } = string.Empty;
public string DefectDescription { get; set; } = string.Empty;
public PowderCoating.Core.Enums.ReworkDiscoveredBy DiscoveredBy { get; set; }
public string DiscoveredByDisplay { get; set; } = string.Empty;
public DateTime DiscoveredDate { get; set; }
public string? ReportedByName { get; set; }
public decimal EstimatedReworkCost { get; set; }
public decimal ActualReworkCost { get; set; }
public bool IsBillableToCustomer { get; set; }
public string? BillingNotes { get; set; }
public PowderCoating.Core.Enums.ReworkPricingType? ReworkPricingType { get; set; }
public PowderCoating.Core.Enums.ReworkStatus Status { get; set; }
public string StatusDisplay { get; set; } = string.Empty;
public string StatusColorClass { get; set; } = "secondary";
public PowderCoating.Core.Enums.ReworkResolution? Resolution { get; set; }
public string? ResolutionDisplay { get; set; }
public DateTime? ResolvedDate { get; set; }
public string? ResolutionNotes { get; set; }
public DateTime CreatedAt { get; set; }
}
public class CreateReworkRecordDto
{
public int JobId { get; set; }
public int? JobItemId { get; set; }
public PowderCoating.Core.Enums.ReworkType ReworkType { get; set; }
public PowderCoating.Core.Enums.ReworkReason Reason { get; set; }
public string DefectDescription { get; set; } = string.Empty;
public PowderCoating.Core.Enums.ReworkDiscoveredBy DiscoveredBy { get; set; }
public DateTime DiscoveredDate { get; set; } = DateTime.Today;
public string? ReportedByName { get; set; }
public decimal EstimatedReworkCost { get; set; }
public bool IsBillableToCustomer { get; set; }
public string? BillingNotes { get; set; }
// Rework job creation (opt-in)
public bool CreateReworkJob { get; set; }
public List<int>? ReworkJobItemIds { get; set; } // null = not creating a job
public PowderCoating.Core.Enums.ReworkPricingType? ReworkPricingType { get; set; }
}
public class UpdateReworkRecordDto
{
public int Id { get; set; }
public PowderCoating.Core.Enums.ReworkStatus Status { get; set; }
public PowderCoating.Core.Enums.ReworkResolution? Resolution { get; set; }
public decimal ActualReworkCost { get; set; }
public bool IsBillableToCustomer { get; set; }
public string? BillingNotes { get; set; }
public DateTime? ResolvedDate { get; set; }
public string? ResolutionNotes { get; set; }
public int? ReworkJobId { get; set; }
}
// ViewModel for the Edit Items wizard page
public class JobEditItemsViewModel
{
public int JobId { get; set; }
public string JobNumber { get; set; } = string.Empty;
public int? CustomerId { get; set; }
public decimal TaxPercent { get; set; }
public int? OvenCostId { get; set; }
public int OvenBatches { get; set; } = 1;
public int? OvenCycleMinutes { get; set; }
public List<CreateQuoteItemDto> JobItems { get; set; } = new();
}
// DTO for the part intake / receiving form
public class IntakeJobDto
{
[Required]
public int JobId { get; set; }
[Display(Name = "Actual Part Count")]
[Range(0, 10000, ErrorMessage = "Part count must be between 0 and 10,000")]
public int? ActualPartCount { get; set; }
[StringLength(2000, ErrorMessage = "Condition notes cannot exceed 2000 characters")]
[Display(Name = "Condition Notes")]
public string? ConditionNotes { get; set; }
[Display(Name = "Advance status to In Preparation")]
public bool AdvanceToInPreparation { get; set; } = true;
}