Files
PowderCoatingLogix/src/PowderCoating.Application/Mappings/JobProfile.cs
T
spouliot 6569d9c4ea Add SMS gating, TCPA terms agreement, and compose-before-send modal
- Three-tier SMS gate: platform kill-switch → admin force-disable → plan AllowSms → company opt-in
- CompanySmsAgreement entity records admin acceptance of TCPA terms with IP, user agent, and terms version
- SMS terms of service modal on Company Settings with versioned re-agreement (AppConstants.SmsTermsVersion)
- Dev redirect: non-production SMS routed to Twilio:DevRedirectPhone to protect real customer numbers
- Removed redundant Ready for Pickup SMS (Job Completed covers it)
- Role-based compose modal on job completion: Admin/Manager reviews and edits before send; ShopFloor auto-sends
- Send SMS button on job details for ad-hoc messages (Admin/Manager only)
- SendJobSmsAsync auto-appends STOP opt-out language if missing
- Migrations: AddSmsGating, AddCompanySmsAgreement

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 22:29:39 -04:00

241 lines
15 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.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.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.Worker != null ? src.Worker.Name : string.Empty))
.ForMember(dest => dest.WorkerRole, opt => opt.MapFrom(src => src.Worker != null ? FormatEnumName(src.Worker.Role.ToString()) : 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.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));
// 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"
};
}