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 workOrder = new WorkOrder { InternalWONumber = woNumber, WorkerOrderNumber = woNumber, WorkOrderType = request.WorkOrderType, SiteCode = siteCode, Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim(), Trade = string.IsNullOrWhiteSpace(request.Trade) ? null : request.Trade.Trim(), 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 }; 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.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 (request.VendorId.HasValue || request.ApptDate.HasValue || !string.IsNullOrWhiteSpace(request.ApptTime)) { 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 }; _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); } } _context.workOrders.Add(workOrder); await _auditService.StageCreatedAsync(workOrder.Id, actorId, woNumber); await StageChangesAsync(workOrder.Id, changes, actorId, dispatch); await _context.SaveChangesAsync(); if (dispatch != null && workOrder.PrimaryDispatchId == null) { workOrder.PrimaryDispatchId = dispatch.Id; dispatch.WorkOrderId = workOrder.Id; await _context.SaveChangesAsync(); } if (request.PocContactId.HasValue) { _context.WorkOrderContacts.Add(new WorkOrderContacts { WorkorderId = workOrder.Id, ContactId = request.PocContactId, Notes = string.IsNullOrWhiteSpace(request.PocNotes) ? null : request.PocNotes.Trim() }); 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 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 async Task StageChangesAsync( 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: await _auditService.StageStatusChangedAsync(workOrderId, change.OldValue, change.NewValue, actorId); break; case AuditActionType.AssignmentChanged: await _auditService.StageAssignmentChangedAsync(workOrderId, change.OldValue, change.NewValue, actorId); break; default: await _auditService.StageFieldChangedAsync(workOrderId, change.FieldName, change.OldValue ?? "", change.NewValue, actorId, dispatchId); break; } } } } }