Files
PowderCoatingLogix/src/PowderCoating.Application/Mappings/JobProfile.cs
T
spouliot f018653c18 Add rework pricing type (Fixed vs Per-Item) and inline rework flow on Job Details
Adds a PricingType enum to ReworkRecord (FixedPrice | PerItem), surfaces the
choice in the rework modal on Job Details, and wires the resulting unit/total
price display. Includes migration AddReworkPricingType, updated repository
query for rework history, and help article updates.

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

250 lines
16 KiB
C#

using AutoMapper;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
using PowderCoating.Application.DTOs.Job;
namespace PowderCoating.Application.Mappings;
public class JobProfile : Profile
{
public JobProfile()
{
// Job to JobDto - Map all lookup properties from navigation
CreateMap<Job, JobDto>()
.ForMember(dest => dest.CustomerName,
opt => opt.MapFrom(src => src.Customer != null
? (!string.IsNullOrWhiteSpace(src.Customer.CompanyName)
? src.Customer.CompanyName
: $"{src.Customer.ContactFirstName} {src.Customer.ContactLastName}".Trim())
: string.Empty))
.ForMember(dest => dest.CustomerCompanyName,
opt => opt.MapFrom(src => src.Customer != null ? src.Customer.CompanyName : null))
.ForMember(dest => dest.CustomerContactName,
opt => opt.MapFrom(src => src.Customer != null
? $"{src.Customer.ContactFirstName} {src.Customer.ContactLastName}".Trim()
: null))
.ForMember(dest => dest.AssignedWorkerName,
opt => opt.MapFrom(src => src.AssignedUser != null ? src.AssignedUser.FullName : null))
// Status lookup mappings
.ForMember(dest => dest.JobStatusId, opt => opt.MapFrom(src => src.JobStatusId))
.ForMember(dest => dest.StatusCode, opt => opt.MapFrom(src => src.JobStatus.StatusCode))
.ForMember(dest => dest.StatusDisplayName, opt => opt.MapFrom(src => src.JobStatus.DisplayName))
.ForMember(dest => dest.StatusColorClass, opt => opt.MapFrom(src => src.JobStatus.ColorClass))
.ForMember(dest => dest.StatusIconClass, opt => opt.MapFrom(src => src.JobStatus.IconClass))
.ForMember(dest => dest.StatusDisplayOrder, opt => opt.MapFrom(src => src.JobStatus.DisplayOrder))
.ForMember(dest => dest.StatusIsTerminal, opt => opt.MapFrom(src => src.JobStatus.IsTerminalStatus))
.ForMember(dest => dest.StatusIsWIP, opt => opt.MapFrom(src => src.JobStatus.IsWorkInProgressStatus))
// Priority lookup mappings
.ForMember(dest => dest.JobPriorityId, opt => opt.MapFrom(src => src.JobPriorityId))
.ForMember(dest => dest.PriorityCode, opt => opt.MapFrom(src => src.JobPriority.PriorityCode))
.ForMember(dest => dest.PriorityDisplayName, opt => opt.MapFrom(src => src.JobPriority.DisplayName))
.ForMember(dest => dest.PriorityColorClass, opt => opt.MapFrom(src => src.JobPriority.ColorClass))
.ForMember(dest => dest.PriorityIconClass, opt => opt.MapFrom(src => src.JobPriority.IconClass))
.ForMember(dest => dest.PriorityDisplayOrder, opt => opt.MapFrom(src => src.JobPriority.DisplayOrder))
// Oven selection
.ForMember(dest => dest.OvenLabel,
opt => opt.MapFrom(src => src.OvenCost != null ? src.OvenCost.Label : null))
.ForMember(dest => dest.Items,
opt => opt.MapFrom(src => src.JobItems))
// Prep services mappings
.ForMember(dest => dest.PrepServices, opt => opt.MapFrom(src =>
src.JobPrepServices.Select(jps => jps.PrepService).ToList()))
.ForMember(dest => dest.PrepServiceIds, opt => opt.MapFrom(src =>
src.JobPrepServices.Select(jps => jps.PrepServiceId).ToList()))
.ForMember(dest => dest.TimeEntries, opt => opt.MapFrom(src => src.TimeEntries))
.ForMember(dest => dest.DiscountType, opt => opt.MapFrom(src => src.DiscountType.ToString()))
.ForMember(dest => dest.IsReworkJob, opt => opt.MapFrom(src => src.IsReworkJob))
.ForMember(dest => dest.OriginalJobId, opt => opt.MapFrom(src => src.OriginalJobId))
.ForMember(dest => dest.OriginalJobNumber,
opt => opt.MapFrom(src => src.OriginalJob != null ? src.OriginalJob.JobNumber : null))
.ForMember(dest => dest.IntakeCheckedByName,
opt => opt.MapFrom(src => src.IntakeCheckedBy != null ? src.IntakeCheckedBy.FullName : null))
.ForMember(dest => dest.CustomerEmail,
opt => opt.MapFrom(src => src.Customer != null ? src.Customer.Email : null))
.ForMember(dest => dest.CustomerNotifyByEmail,
opt => opt.MapFrom(src => src.Customer == null || src.Customer.NotifyByEmail))
.ForMember(dest => dest.CustomerNotifyBySms,
opt => opt.MapFrom(src => src.Customer != null && src.Customer.NotifyBySms))
.ForMember(dest => dest.CustomerMobilePhone,
opt => opt.MapFrom(src => src.Customer != null
? (src.Customer.MobilePhone ?? src.Customer.Phone)
: null));
// JobTimeEntry → JobTimeEntryDto
CreateMap<JobTimeEntry, JobTimeEntryDto>()
.ForMember(dest => dest.WorkerName, opt => opt.MapFrom(src =>
src.UserDisplayName ?? string.Empty));
// CreateJobDto to Job
CreateMap<CreateJobDto, Job>()
.ForMember(dest => dest.JobNumber, opt => opt.Ignore()) // Generated by system
.ForMember(dest => dest.JobStatus, opt => opt.Ignore()) // Will be set by FK
.ForMember(dest => dest.JobPriority, opt => opt.Ignore()) // Will be set by FK
.ForMember(dest => dest.OvenCost, opt => opt.Ignore()) // Will be set by FK
.ForMember(dest => dest.JobPrepServices, opt => opt.Ignore()) // Handled separately
.ForMember(dest => dest.JobItems, opt => opt.Ignore()); // Handled separately
// UpdateJobDto to Job
CreateMap<UpdateJobDto, Job>()
.ForMember(dest => dest.JobNumber, opt => opt.Ignore()) // Cannot be changed
.ForMember(dest => dest.JobStatus, opt => opt.Ignore()) // Will be set by FK
.ForMember(dest => dest.JobPriority, opt => opt.Ignore()) // Will be set by FK
.ForMember(dest => dest.OvenCost, opt => opt.Ignore()) // Will be set by FK
.ForMember(dest => dest.JobPrepServices, opt => opt.Ignore()) // Handled separately
.ForMember(dest => dest.JobItems, opt => opt.Ignore()); // Handled separately
// Job to UpdateJobDto (needed for Edit GET action)
CreateMap<Job, UpdateJobDto>()
.ForMember(dest => dest.JobStatusId, opt => opt.MapFrom(src => src.JobStatusId))
.ForMember(dest => dest.JobPriorityId, opt => opt.MapFrom(src => src.JobPriorityId))
.ForMember(dest => dest.PrepServiceIds, opt => opt.MapFrom(src =>
src.JobPrepServices.Select(jps => jps.PrepServiceId).ToList()));
// Job to JobListDto
CreateMap<Job, JobListDto>()
.ForMember(dest => dest.CustomerName,
opt => opt.MapFrom(src => src.Customer != null
? (!string.IsNullOrWhiteSpace(src.Customer.CompanyName)
? src.Customer.CompanyName
: $"{src.Customer.ContactFirstName} {src.Customer.ContactLastName}".Trim())
: string.Empty))
.ForMember(dest => dest.AssignedWorkerName,
opt => opt.MapFrom(src => src.AssignedUser != null ? src.AssignedUser.FullName : null))
// Status lookup mappings
.ForMember(dest => dest.JobStatusId, opt => opt.MapFrom(src => src.JobStatusId))
.ForMember(dest => dest.StatusCode, opt => opt.MapFrom(src => src.JobStatus.StatusCode))
.ForMember(dest => dest.StatusDisplayName, opt => opt.MapFrom(src => src.JobStatus.DisplayName))
.ForMember(dest => dest.StatusColorClass, opt => opt.MapFrom(src => src.JobStatus.ColorClass))
.ForMember(dest => dest.StatusIsWIP, opt => opt.MapFrom(src => src.JobStatus.IsWorkInProgressStatus))
// Priority lookup mappings
.ForMember(dest => dest.JobPriorityId, opt => opt.MapFrom(src => src.JobPriorityId))
.ForMember(dest => dest.PriorityCode, opt => opt.MapFrom(src => src.JobPriority.PriorityCode))
.ForMember(dest => dest.PriorityDisplayName, opt => opt.MapFrom(src => src.JobPriority.DisplayName))
.ForMember(dest => dest.PriorityColorClass, opt => opt.MapFrom(src => src.JobPriority.ColorClass))
.ForMember(dest => dest.CustomerEmail,
opt => opt.MapFrom(src => src.Customer != null ? src.Customer.Email : null))
.ForMember(dest => dest.CustomerNotifyByEmail,
opt => opt.MapFrom(src => src.Customer == null || src.Customer.NotifyByEmail));
// JobItem mappings
CreateMap<JobItem, JobItemDto>()
.ForMember(dest => dest.Coats, opt => opt.MapFrom(src => src.Coats))
.ForMember(dest => dest.PrepServices, opt => opt.MapFrom(src => src.PrepServices));
CreateMap<JobItemPrepService, JobItemPrepServiceDto>()
.ForMember(dest => dest.PrepServiceName,
opt => opt.MapFrom(src => src.PrepService != null ? src.PrepService.ServiceName : null));
CreateMap<CreateJobItemDto, JobItem>()
.ForMember(dest => dest.TotalPrice,
opt => opt.MapFrom(src => src.UnitPrice * src.Quantity));
CreateMap<UpdateJobItemDto, JobItem>();
CreateMap<JobItem, UpdateJobItemDto>();
// JobItemCoat mappings
CreateMap<JobItemCoat, JobItemCoatDto>()
.ForMember(dest => dest.VendorName,
opt => opt.MapFrom(src => src.Vendor != null ? src.Vendor.CompanyName : null));
// Job to ShopFloorJobDto
CreateMap<Job, ShopFloorJobDto>()
.ForMember(dest => dest.CustomerName,
opt => opt.MapFrom(src => src.Customer != null
? (!string.IsNullOrWhiteSpace(src.Customer.CompanyName)
? src.Customer.CompanyName
: $"{src.Customer.ContactFirstName} {src.Customer.ContactLastName}".Trim())
: string.Empty))
.ForMember(dest => dest.AssignedWorkerName,
opt => opt.MapFrom(src => src.AssignedUser != null ? src.AssignedUser.FullName : null))
// Status lookup mappings
.ForMember(dest => dest.JobStatusId, opt => opt.MapFrom(src => src.JobStatusId))
.ForMember(dest => dest.StatusCode, opt => opt.MapFrom(src => src.JobStatus.StatusCode))
.ForMember(dest => dest.StatusDisplayName, opt => opt.MapFrom(src => src.JobStatus.DisplayName))
.ForMember(dest => dest.StatusColorClass, opt => opt.MapFrom(src => src.JobStatus.ColorClass))
// Priority lookup mappings
.ForMember(dest => dest.JobPriorityId, opt => opt.MapFrom(src => src.JobPriorityId))
.ForMember(dest => dest.PriorityCode, opt => opt.MapFrom(src => src.JobPriority.PriorityCode))
.ForMember(dest => dest.PriorityDisplayName, opt => opt.MapFrom(src => src.JobPriority.DisplayName))
.ForMember(dest => dest.PriorityColorClass, opt => opt.MapFrom(src => src.JobPriority.ColorClass))
.ForMember(dest => dest.ItemCount,
opt => opt.MapFrom(src => src.JobItems.Count));
// Note: NextSteps will be removed - was workflow logic that will be reimplemented later
// JobPhoto mappings
CreateMap<JobPhoto, JobPhotoDto>()
.ForMember(dest => dest.PhotoTypeDisplay,
opt => opt.MapFrom(src => FormatEnumName(src.PhotoType.ToString())))
.ForMember(dest => dest.UploadedByName,
opt => opt.MapFrom(src => src.UploadedBy != null ? src.UploadedBy.FullName : "Unknown"));
CreateMap<UploadJobPhotoDto, JobPhoto>();
CreateMap<UpdateJobPhotoDto, JobPhoto>();
// ReworkRecord → ReworkRecordDto
CreateMap<ReworkRecord, ReworkRecordDto>()
.ForMember(dest => dest.ReworkTypeDisplay,
opt => opt.MapFrom(src => FormatEnumName(src.ReworkType.ToString())))
.ForMember(dest => dest.ReasonDisplay,
opt => opt.MapFrom(src => FormatReworkReason(src.Reason)))
.ForMember(dest => dest.DiscoveredByDisplay,
opt => opt.MapFrom(src => src.DiscoveredBy == ReworkDiscoveredBy.Internal ? "Internal (QC)" : "Customer"))
.ForMember(dest => dest.StatusDisplay,
opt => opt.MapFrom(src => FormatEnumName(src.Status.ToString())))
.ForMember(dest => dest.StatusColorClass,
opt => opt.MapFrom(src => GetReworkStatusColor(src.Status)))
.ForMember(dest => dest.ResolutionDisplay,
opt => opt.MapFrom(src => src.Resolution.HasValue ? FormatReworkResolution(src.Resolution.Value) : null))
.ForMember(dest => dest.JobItemDescription,
opt => opt.MapFrom(src => src.JobItem != null ? src.JobItem.Description : null))
.ForMember(dest => dest.ReworkJobNumber,
opt => opt.MapFrom(src => src.ReworkJob != null ? src.ReworkJob.JobNumber : null))
.ForMember(dest => dest.ReworkPricingType,
opt => opt.MapFrom(src => src.ReworkPricingType));
// Job → JobDto (rework fields)
// (IsReworkJob and OriginalJobId map by convention; OriginalJobNumber needs explicit map — handled in controller)
// Job → JobListDto (rework fields map by convention)
// JobChangeHistory -> JobChangeHistoryDto
CreateMap<JobChangeHistory, JobChangeHistoryDto>()
.ForMember(dest => dest.ChangedByName, opt => opt.MapFrom(src =>
src.ChangedBy != null ? $"{src.ChangedBy.FirstName} {src.ChangedBy.LastName}" : "Unknown"));
}
private static string FormatEnumName(string enumName) =>
System.Text.RegularExpressions.Regex.Replace(enumName, "([a-z])([A-Z])", "$1 $2");
private static string FormatReworkReason(ReworkReason reason) => reason switch
{
ReworkReason.AdhesionFailure => "Adhesion Failure",
ReworkReason.Contamination => "Contamination",
ReworkReason.ColorMismatch => "Color Mismatch",
ReworkReason.RunsSags => "Runs / Sags",
ReworkReason.SurfacePrepFailure => "Surface Prep Failure",
ReworkReason.OvenIssue => "Oven Issue",
ReworkReason.InsufficientCoverage => "Insufficient Coverage",
ReworkReason.HandlingDamage => "Handling Damage",
_ => "Other"
};
private static string FormatReworkResolution(ReworkResolution resolution) => resolution switch
{
ReworkResolution.RecoatedNoCharge => "Recoated — No Charge",
ReworkResolution.RecoatedBilled => "Recoated — Billed to Customer",
ReworkResolution.CustomerCredited => "Customer Credited",
ReworkResolution.WrittenOff => "Written Off",
ReworkResolution.NoActionRequired => "No Action Required",
_ => resolution.ToString()
};
private static string GetReworkStatusColor(ReworkStatus status) => status switch
{
ReworkStatus.Open => "danger",
ReworkStatus.InProgress => "warning",
ReworkStatus.Resolved => "success",
ReworkStatus.WrittenOff => "secondary",
ReworkStatus.Disputed => "info",
_ => "secondary"
};
}