using System.Text.Json; using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using FluentValidation; using Microsoft.EntityFrameworkCore; using SeaHaven.DataServices.Interfaces; using SeaHaven.Services.DTOs; using SeaHaven.Services.Exceptions; using SeaHaven.Services.Helpers; using SeaHaven.Services.Interfaces; using SeaHaven.Services.Validation; namespace SeaHaven.Services.Implementation { public class WorkOrderBoardCreateService : IWorkOrderBoardCreateService { private readonly ApplicationDbContext _context; private readonly IWorkOrderBoardDataService _boardDataService; private readonly IWorkOrderBoardService _boardService; private readonly IWorkOrderAuditService _auditService; private readonly IWorkOrderBoardCreateValidation _validator; public WorkOrderBoardCreateService( ApplicationDbContext context, IWorkOrderBoardDataService boardDataService, IWorkOrderBoardService boardService, IWorkOrderAuditService auditService, IWorkOrderBoardCreateValidation validator) { _context = context; _boardDataService = boardDataService; _boardService = boardService; _auditService = auditService; _validator = validator; } public async Task CreateAsync(WorkOrderBoardCreateRequestDto request, string? actorId) { var validationResult = await _validator.ValidateAsync(request); if (!validationResult.IsValid) throw new ValidationException(validationResult.Errors); var woNumber = await ResolveWoNumberAsync(request.WoNumber); var siteCode = request.SiteCode!.Trim(); var primaryService = ResolvePrimaryService(request); var extraServicesJson = SerializeExtraServices(request.ExtraServices); var serviceNotes = TrimOrNull(request.ServiceNotes); var pocName = TrimOrNull(request.PocName); var pocPhone = TrimOrNull(request.PocPhone); var pocNotes = TrimOrNull(request.PocNotes); var techPhone = TrimOrNull(request.TechPhone); var vendorNotes = TrimOrNull(request.VendorNotes); var hasVendorDispatch = request.VendorId.HasValue || request.ApptDate.HasValue || !string.IsNullOrWhiteSpace(request.ApptTime); if (request.VendorId.HasValue && request.VendorId.Value > 0) { var vendorExists = await _context.Vendors.AnyAsync(v => v.Id == request.VendorId.Value && v.IsDeleted != true); if (!vendorExists) throw new WorkOrderBoardValidationException("VendorNotFound", "vendorId does not exist."); } var workOrder = new WorkOrder { InternalWONumber = woNumber, WorkerOrderNumber = woNumber, WorkOrderType = request.WorkOrderType, SiteCode = siteCode, Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim(), Trade = primaryService, ExtraServices = extraServicesJson, ServiceNotes = serviceNotes, PocName = pocName, PocPhone = pocPhone, PocNotes = pocNotes, LocationId = request.LocationId, DueDate = request.DueDate?.Date, AssignTo = string.IsNullOrWhiteSpace(request.AssignTo) ? null : request.AssignTo.Trim(), TargetWeek = request.TargetWeek, ScheduleWeekOnly = request.ScheduleWeekOnly, DocStatus = request.DocStatus, createdby = actorId, CreatedDate = DateTime.UtcNow, istemplate = false }; if (!hasVendorDispatch) { workOrder.TechPhone = techPhone; workOrder.VendorNotes = vendorNotes; } WorkOrderBoardFieldMutations.SetInitialLifecycleStatus(workOrder); var changes = new List { WorkOrderBoardFieldMutations.BoardFieldChange.Changed("InternalWONumber", "", woNumber), WorkOrderBoardFieldMutations.BoardFieldChange.Changed("WorkOrderType", "", request.WorkOrderType.ToString()), WorkOrderBoardFieldMutations.BoardFieldChange.Changed("SiteCode", "", siteCode), WorkOrderBoardFieldMutations.BoardFieldChange.Changed("LifecycleStatus", "", LifecycleStatus.Incomplete.ToString()) }; if (workOrder.Description != null) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("Description", "", workOrder.Description)); if (workOrder.Trade != null) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("Trade", "", workOrder.Trade)); if (workOrder.ExtraServices != null) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("ExtraServices", "", workOrder.ExtraServices)); if (workOrder.ServiceNotes != null) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("ServiceNotes", "", workOrder.ServiceNotes)); if (workOrder.PocName != null) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("PocName", "", workOrder.PocName)); if (workOrder.PocPhone != null) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("PocPhone", "", workOrder.PocPhone)); if (workOrder.PocNotes != null) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("PocNotes", "", workOrder.PocNotes)); if (workOrder.TechPhone != null) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("TechPhone", "", workOrder.TechPhone)); if (workOrder.VendorNotes != null) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("VendorNotes", "", workOrder.VendorNotes)); if (workOrder.LocationId.HasValue) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("LocationId", "", workOrder.LocationId.Value.ToString())); if (workOrder.DueDate.HasValue) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("DueDate", "", workOrder.DueDate.Value.ToString("yyyy-MM-dd"))); if (workOrder.TargetWeek.HasValue) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("TargetWeek", "", workOrder.TargetWeek.Value.ToString("yyyy-MM-dd"))); if (workOrder.ScheduleWeekOnly.HasValue) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("ScheduleWeekOnly", "", workOrder.ScheduleWeekOnly.Value.ToString())); if (workOrder.DocStatus.HasValue) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("DocStatus", "", workOrder.DocStatus.Value.ToString())); if (!string.IsNullOrWhiteSpace(request.AssignTo)) changes.AddRange(WorkOrderBoardFieldMutations.ApplyAssignTo(workOrder, request.AssignTo)); else if (!string.IsNullOrWhiteSpace(workOrder.AssignTo)) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.AssignmentChanged("AssignTo", "", workOrder.AssignTo)); if (request.ScheduledDate.HasValue) changes.AddRange(WorkOrderBoardFieldMutations.ApplyScheduledDate(workOrder, request.ScheduledDate.Value.Date)); Dispatch? dispatch = null; if (hasVendorDispatch) { if (!request.VendorId.HasValue || request.VendorId.Value <= 0) throw new WorkOrderBoardValidationException("DispatchRequired", "vendorId is required when setting appointment fields."); dispatch = new Dispatch { VendorId = request.VendorId.Value, Status = "Pending", CreatedDate = DateTime.UtcNow, ScheduledDate = request.ApptDate?.Date, TechPhone = techPhone, VendorNotes = vendorNotes }; _context.Dispatches.Add(dispatch); workOrder.PrimaryDispatch = dispatch; changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("VendorId", "", request.VendorId.Value.ToString())); if (request.ApptDate.HasValue) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("ApptDate", "", request.ApptDate.Value.ToString("yyyy-MM-dd"))); if (!string.IsNullOrWhiteSpace(request.ApptTime)) { var apptChange = WorkOrderBoardFieldMutations.ApplyApptTime(workOrder, dispatch, request.ApptTime); if (apptChange.HasChanged) changes.Add(apptChange); } if (techPhone != null) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("TechPhone", "", techPhone)); if (vendorNotes != null) changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("VendorNotes", "", vendorNotes)); } _context.workOrders.Add(workOrder); await _context.SaveChangesAsync(); if (dispatch != null && workOrder.PrimaryDispatchId == null) { workOrder.PrimaryDispatchId = dispatch.Id; dispatch.WorkOrderId = workOrder.Id; } _auditService.StageCreated(workOrder.Id, actorId, woNumber); StageChanges(workOrder.Id, changes, actorId, dispatch); if (request.PocContactId.HasValue) { _context.WorkOrderContacts.Add(new WorkOrderContacts { WorkorderId = workOrder.Id, ContactId = request.PocContactId, Notes = pocNotes }); } await _context.SaveChangesAsync(); var row = await _boardService.GetBoardRowAsync(workOrder.Id); return row ?? throw new WorkOrderBoardValidationException("NotFound", "Work order was created but could not be loaded."); } private static string? ResolvePrimaryService(WorkOrderBoardCreateRequestDto request) { if (!string.IsNullOrWhiteSpace(request.PrimaryService)) return request.PrimaryService.Trim(); if (!string.IsNullOrWhiteSpace(request.Trade)) return request.Trade.Trim(); return null; } private static string? SerializeExtraServices(List? extras) { if (extras == null || extras.Count == 0) return null; var normalized = new List(); var seen = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var item in extras) { if (string.IsNullOrWhiteSpace(item)) continue; var trimmed = item.Trim(); if (trimmed.Length > 128) throw new WorkOrderBoardValidationException( "InvalidValue", "Each extra service must be at most 128 characters."); if (!seen.Add(trimmed)) continue; normalized.Add(trimmed); } if (normalized.Count == 0) return null; var json = JsonSerializer.Serialize(normalized); if (json.Length > 2000) throw new WorkOrderBoardValidationException( "InvalidValue", "extraServices must serialize to at most 2000 characters."); return json; } private static string? TrimOrNull(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim(); private async Task ResolveWoNumberAsync(string? requested) { if (!string.IsNullOrWhiteSpace(requested)) { if (!WorkOrderNumberNormalizer.TryNormalize(requested, out var normalized, out var error)) throw new WorkOrderBoardValidationException("InvalidWoNumber", error ?? "Invalid WO number."); if (await _boardDataService.InternalWoNumberExistsAsync(normalized, 0)) throw new WorkOrderBoardValidationException("DuplicateWoNumber", "WO number already exists."); return normalized; } for (var attempt = 0; attempt < 20; attempt++) { var candidate = await GenerateNextSequentialWoNumberAsync(); if (!await _boardDataService.InternalWoNumberExistsAsync(candidate, 0)) return candidate; } throw new WorkOrderBoardValidationException("WoNumberGenerationFailed", "Could not generate a unique WO number."); } private async Task GenerateNextSequentialWoNumberAsync() { var maxId = await _context.workOrders.MaxAsync(w => (int?)w.Id) ?? 0; var seed = Math.Max(maxId + 1, 1); if (!WorkOrderNumberNormalizer.TryNormalize(seed.ToString(), out var normalized, out _)) normalized = seed.ToString().PadLeft(11, '0'); return normalized; } private void StageChanges( int workOrderId, IEnumerable changes, string? actorId, Dispatch? dispatch) { foreach (var change in changes.Where(c => c.HasChanged)) { var dispatchId = change.DispatchId ?? (dispatch?.Id > 0 ? dispatch.Id : null); switch (change.Action) { case AuditActionType.StatusChanged: _auditService.StageStatusChanged(workOrderId, change.OldValue, change.NewValue, actorId); break; case AuditActionType.AssignmentChanged: _auditService.StageAssignmentChanged(workOrderId, change.OldValue, change.NewValue, actorId); break; default: _auditService.StageFieldChanged(workOrderId, change.FieldName, change.OldValue ?? "", change.NewValue, actorId, dispatchId); break; } } } } }