Files
PowderCoatingLogix/src/PowderCoating.Application/DTOs/Job/JobDtos.cs
T
spouliot 539c6c2559 Fix oven batch conversion, invoice quantity, AI photo pricing, and enforce pricing flag propagation
- Carry OvenBatches/OvenCycleMinutes from Quote → Job entity (was missing fields; all job pricing recalcs hardcoded 1/null)
- Fix invoice creation from job always showing Quantity=1 (was using TotalPrice as UnitPrice with qty 1)
- Add IsAiItem to JobItem + migration; map in all 3 JobItemAssemblyService.CreateJobItem overloads so AI photo jobs no longer double-price on first edit after quote→job conversion
- Propagate IsAiItem through all existingItemsData JSON blocks in Jobs views (Edit, EditItems, Create) so the wizard preserves AI routing on re-edit
- Add PricingRoutingFlags_ExistOnBothQuoteItemAndJobItem structural test + 3 behavioral IsAiItem tests to JobItemAssemblyServiceTests
- Consolidate item wizard partials (_ItemWizardModal, _SqFtCalculatorModal) and item-wizard.css into shared locations
- Document pricing flag propagation checklist in CLAUDE.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 16:54:22 -04:00

541 lines
20 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; }
[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; }
[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 string? Sku { 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 string? Notes { get; set; }
}
// DTO for completing a job
public class CompleteJobDto
{
public int JobId { get; set; }
public decimal? ActualTimeSpentHours { get; set; }
public List<JobItemCoatUsageDto> CoatUsages { 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 coat
public class JobItemCoatUsageDto
{
public int JobItemCoatId { 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.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; }
}
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;
}