Files
PowderCoatingLogix/src/PowderCoating.Application/DTOs/Job/JobDtos.cs
T
spouliot df504674e9 Add oven/batch settings to job create and edit forms
CreateJobDto and UpdateJobDto now carry OvenCostId, OvenBatches, and
OvenCycleMinutes. The Create POST sets these on the new Job entity and
passes them to the pricing engine; the Edit GET populates them from the
existing job so the form reflects saved values, and the Edit POST writes
them back before repricing.

Both Jobs/Create.cshtml and Jobs/Edit.cshtml now include an Oven & Batch
Settings card (matching the quote form) with oven selector, batch count,
and cycle time inputs. The wizard init block now passes the selected
OvenCostId instead of null so live auto-pricing reflects the oven cost.

ViewBag.DefaultOvenCycleMinutes added to PopulateCreateEditWizardViewBagsAsync
so the placeholder in both views shows the company default.

Also fixed: NoExtraLayerCharge was missing from the Edit GET coat DTO
mapping (would have caused the flag to reset to false on next edit).

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

559 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 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 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.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;
}