using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using Microsoft.Extensions.Options; using SeaHaven.DataServices.Interfaces; using SeaHaven.DataServices.Models; using SeaHaven.Services.Configuration; using SeaHaven.Services.DTOs; using SeaHaven.Services.Interfaces; namespace SeaHaven.Services.Implementation { public class WorkOrderDispatchService : IWorkOrderDispatchService { private readonly IWorkOrderDataService _workOrderDataService; private readonly IDispatchDataService _dispatchDataService; private readonly IVendorDataService _vendorDataService; private readonly IUserDataService _userDataService; private readonly ITaskListTemplateDataService _taskListTemplateDataService; private readonly ICommentDataService _commentDataService; private readonly IDispatchEmailPort _emailPort; private readonly IVendorTokenPort _vendorTokenPort; private readonly FrontendOptions _frontendOptions; public WorkOrderDispatchService( IWorkOrderDataService workOrderDataService, IDispatchDataService dispatchDataService, IVendorDataService vendorDataService, IUserDataService userDataService, ITaskListTemplateDataService taskListTemplateDataService, ICommentDataService commentDataService, IDispatchEmailPort emailPort, IVendorTokenPort vendorTokenPort, IOptions frontendOptions) { _workOrderDataService = workOrderDataService; _dispatchDataService = dispatchDataService; _vendorDataService = vendorDataService; _userDataService = userDataService; _taskListTemplateDataService = taskListTemplateDataService; _commentDataService = commentDataService; _emailPort = emailPort; _vendorTokenPort = vendorTokenPort; _frontendOptions = frontendOptions.Value; } public async Task DispatchToVendorAsync(DispatchToVendorInput input, CancellationToken cancellationToken) { var workOrders = await _workOrderDataService.GetByRequestedIdsWithDetailsAsync( input.WorkOrderIds, cancellationToken); if (workOrders.Count == 0) throw new InvalidOperationException("No work orders found"); var primaryWO = workOrders[0]; var lastPO = await _dispatchDataService.GenerateNextPONumberAsync(); int nextPO = int.TryParse(lastPO, out var po) ? po : 1; var lastDSP = await _dispatchDataService.GenerateNextDispatchNumberAsync(); int nextDSP = lastDSP.StartsWith("DSP-") && int.TryParse(lastDSP.Substring(4), out var dspNum) ? dspNum : 1; var currentUser = input.UserId != null ? await _userDataService.GetByIdAsync(input.UserId) : null; var senderName = currentUser != null ? (currentUser.FirstName + " " + currentUser.LastName).Trim() : ""; var templateItemTexts = await LoadTemplateItemTextsAsync(input.TaskListTemplateId); var distinctVendorIds = input.VendorIds.Distinct().ToList(); var vendors = await _vendorDataService.GetByIdsOrderedAsync(distinctVendorIds, cancellationToken); var dispatches = new List(); foreach (var vendor in vendors) { var poNumber = $"VPO-{nextPO:D5}"; nextPO++; var dispatchNumber = $"DSP-{nextDSP:D5}"; nextDSP++; var replyTo = $"wo-{primaryWO.InternalWONumber}-{dispatchNumber}@int.seahaven.com"; var dispatch = new Dispatch { WorkOrderId = primaryWO.Id, VendorId = vendor.Id, DispatchNumber = dispatchNumber, PONumber = poNumber, NTEAmount = input.NTEAmount, Description = input.Description, Status = "Sent", ScheduledDate = input.ScheduledDate, DispatchedAt = DateTime.UtcNow, ReplyToAddress = replyTo, DispatchWorkOrders = workOrders.Select(wo => new DispatchWorkOrder { WorkOrderId = wo.Id }).ToList() }; var checklistItems = BuildChecklistItems(templateItemTexts, input.CustomChecklistItems); dispatch.ChecklistItems = checklistItems; var woListHtml = string.Join("", workOrders.Select(wo => $"{System.Net.WebUtility.HtmlEncode(wo.InternalWONumber ?? "")}" + $"{System.Net.WebUtility.HtmlEncode(wo.WorkerOrderTitle ?? "")}" + $"{System.Net.WebUtility.HtmlEncode(wo.Priority ?? "")}" + $"{wo.DueDate?.ToString("yyyy-MM-dd") ?? "N/A"}")); var locationName = System.Net.WebUtility.HtmlEncode(primaryWO.Locations?.Name ?? ""); var locationAddr = System.Net.WebUtility.HtmlEncode(primaryWO.Locations?.Address1 ?? ""); var subject = $"[{dispatchNumber}] {(workOrders.Count > 1 ? $"{workOrders.Count} Work Orders" : primaryWO.WorkerOrderTitle)} at {primaryWO.Locations?.Name ?? ""}"; var descriptionHtml = System.Net.WebUtility.HtmlEncode(input.Description ?? primaryWO.Description ?? ""); var senderNameHtml = System.Net.WebUtility.HtmlEncode(senderName); var vendorToken = await _vendorTokenPort.GetOrCreateActiveTokenAsync(vendor.Id); var frontendBase = _frontendOptions.FrontendBaseUrl?.TrimEnd('/') ?? ""; var portalUrl = $"{frontendBase}/v/{vendorToken}/dashboard"; var html = $@"

Work Order Dispatch — {dispatchNumber}

Location: {locationName} {locationAddr}

Vendor PO: {poNumber}   NTE: ${input.NTEAmount?.ToString("F2") ?? "N/A"}

Work Orders

{woListHtml}
WO # Title Priority Due Date

Description

{descriptionHtml}


View Work Order

{senderNameHtml}
Sea Haven Industries


Reply to this email to communicate about this dispatch.

"; var emailSent = await _emailPort.SendDispatchEmailAsync(vendor.Email!, subject, html, replyTo); dispatch.EmailSent = emailSent; var savedDispatch = await _dispatchDataService.AddAsync(dispatch); var auditLogs = workOrders.Select(wo => new WorkOrderAuditLog { WorkOrderId = wo.Id, UserId = input.UserId, FieldName = "Dispatch", OldValue = "", NewValue = $"{vendor.CompanyName} ({dispatchNumber})", Action = AuditAction.Dispatch.ToString(), CreatedAt = DateTime.UtcNow }).ToList(); await _workOrderDataService.AddAuditLogsAsync(auditLogs); dispatches.Add(new DispatchResultItem { Id = savedDispatch.Id, DispatchNumber = dispatchNumber, PONumber = poNumber, VendorName = vendor.CompanyName, Status = savedDispatch.Status, WorkOrderCount = workOrders.Count }); } return new DispatchToVendorResult { Message = "Dispatched", Dispatches = dispatches }; } private async Task?> LoadTemplateItemTextsAsync(int? taskListTemplateId) { if (!taskListTemplateId.HasValue) return null; var template = await _taskListTemplateDataService.GetByIdWithItemsAsync(taskListTemplateId.Value); if (template?.Items == null) return null; return template.Items .OrderBy(i => i.SortOrder) .Select(i => i.ItemText) .ToList(); } private static List BuildChecklistItems(List? templateItemTexts, List? customChecklistItems) { var checklistItems = new List(); if (templateItemTexts != null) { checklistItems.AddRange(templateItemTexts.Select((text, idx) => new DispatchChecklistItem { ItemText = text, SortOrder = idx })); } if (customChecklistItems != null) { var startIdx = checklistItems.Count; checklistItems.AddRange(customChecklistItems.Select((text, idx) => new DispatchChecklistItem { ItemText = text, SortOrder = startIdx + idx })); } return checklistItems; } public async Task> GetDispatchesAsync(int workOrderId) { var dispatches = await _dispatchDataService.GetByWorkOrderIdAsync(workOrderId); return dispatches.OrderByDescending(d => d.DispatchedAt).Select(d => new DispatchReadModel { Id = d.Id, DispatchNumber = d.DispatchNumber, PONumber = d.PONumber, NTEAmount = d.NTEAmount, Description = d.Description, Status = d.Status, ScheduledDate = d.ScheduledDate, CompletedDate = d.CompletedDate, DispatchedAt = d.DispatchedAt, EmailSent = d.EmailSent, VendorName = d.Vendor != null ? d.Vendor.CompanyName : "", VendorEmail = d.Vendor != null ? d.Vendor.Email : "", VendorId = d.VendorId }).ToList(); } public async Task GetDispatchDetailAsync(int id) { return await _dispatchDataService.GetDispatchDetailAsync(id); } public async Task UpdateDispatchAsync(UpdateDispatchInput input) { var dispatch = await _dispatchDataService.GetByIdAsync(input.Id); if (dispatch == null) return false; if (dispatch.Status != input.Status) { await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog { WorkOrderId = dispatch.WorkOrderId ?? 0, UserId = input.UserId, FieldName = $"Dispatch {dispatch.DispatchNumber} Status", OldValue = dispatch.Status ?? "", NewValue = input.Status ?? "", Action = "dispatch_status_change", CreatedAt = DateTime.UtcNow }); } dispatch.Status = input.Status; dispatch.NTEAmount = input.NTEAmount; dispatch.ScheduledDate = input.ScheduledDate; dispatch.CompletedDate = input.CompletedDate; dispatch.Description = input.Description; if (input.Status == "Completed" && dispatch.CompletedDate == null) dispatch.CompletedDate = DateTime.UtcNow; await _dispatchDataService.UpdateAsync(dispatch); return true; } public async Task AddDispatchCommentAsync(AddDispatchCommentInput input) { var dispatch = await _dispatchDataService.GetByIdWithVendorAndWorkOrderAsync(input.DispatchId); if (dispatch == null) return null; var user = !string.IsNullOrEmpty(input.UserId) ? await _userDataService.GetByIdAsync(input.UserId) : null; var senderName = user != null ? (user.FirstName + " " + user.LastName).Trim() : ""; var comment = new Comments { CreatedDate = DateTime.UtcNow, UserId = input.UserId, WorkerOrderId = dispatch.WorkOrderId, DispatchId = dispatch.Id, Commenttext = input.Text, CommentType = "dispatcher", Commenter = !string.IsNullOrWhiteSpace(senderName) ? senderName : null, }; await _commentDataService.AddAsync(comment); if (input.SendEmail && dispatch.Vendor?.Email != null && dispatch.WorkOrder != null) { var subject = $"[WO-{dispatch.WorkOrder.InternalWONumber}-{dispatch.DispatchNumber}] {dispatch.WorkOrder.WorkerOrderTitle}"; var html = $"

{input.Text}


{senderName}
Sea Haven Industries


Reply to this email to respond.

"; await _emailPort.SendDispatchEmailAsync(dispatch.Vendor.Email, subject, html, dispatch.ReplyToAddress); } return new AddDispatchCommentResult { Id = comment.Id, CreatedDate = comment.CreatedDate, Commenttext = comment.Commenttext, CommentType = comment.CommentType, UserName = user != null ? (user.FirstName + " " + user.LastName).Trim() : "" }; } public async Task VerifyDispatchAsync(int dispatchId, string userId) { var dispatch = await _dispatchDataService.GetByIdAsync(dispatchId); if (dispatch == null) return null; var checklistItems = await _dispatchDataService.GetChecklistItemsAsync(dispatchId); var incompleteItems = checklistItems.Count(c => !c.IsCompleted); var signoffs = await _dispatchDataService.GetSignoffsAsync(dispatchId); var signoffList = signoffs.ToList(); var signoffCount = signoffList.Count; var existingTypes = signoffList.Select(s => s.SignoffType).ToList(); var missing = new List(); if (incompleteItems > 0) missing.Add($"{incompleteItems} checklist item(s) incomplete"); if (signoffCount < 2) { if (!existingTypes.Contains("Customer")) missing.Add("Missing Customer sign-off"); if (!existingTypes.Contains("Vendor")) missing.Add("Missing Vendor sign-off"); } if (missing.Count > 0) throw new DispatchVerificationException(missing); var user = userId != null ? await _userDataService.GetByIdAsync(userId) : null; var userName = user != null ? (user.FirstName + " " + user.LastName).Trim() : ""; dispatch.Status = "Verified"; dispatch.VerifiedBy = userName; dispatch.VerifiedAt = DateTime.UtcNow; await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog { WorkOrderId = dispatch.WorkOrderId ?? 0, UserId = userId, FieldName = $"Dispatch {dispatch.DispatchNumber}", OldValue = dispatch.Status ?? "", NewValue = "Verified", Action = "dispatch_verified", CreatedAt = DateTime.UtcNow }); await _dispatchDataService.UpdateAsync(dispatch); return new VerifyDispatchResult { Message = "Dispatch verified", VerifiedBy = userName, VerifiedAt = dispatch.VerifiedAt }; } public async Task AddDispatchSignoffAsync(AddDispatchSignoffInput input) { var dispatch = await _dispatchDataService.GetByIdAsync(input.DispatchId); if (dispatch == null) return null; var existing = await _dispatchDataService.HasSignoffTypeAsync(input.DispatchId, input.SignoffType!); if (existing) throw new InvalidOperationException($"{input.SignoffType} sign-off already exists"); var signoff = new DispatchSignoff { DispatchId = input.DispatchId, SignoffType = input.SignoffType, Name = input.Name, Signature = input.Signature, SignatureMethod = input.SignatureMethod, SignedAt = DateTime.UtcNow }; await _dispatchDataService.AddSignoffAsync(signoff); return new AddDispatchSignoffResult { Id = signoff.Id, SignoffType = signoff.SignoffType, Name = signoff.Name, SignedAt = signoff.SignedAt, SignatureMethod = signoff.SignatureMethod }; } public async Task UpdateChecklistItemAsync(ChecklistItemUpdateInput input) { var item = await _dispatchDataService.GetChecklistItemByIdAsync(input.Id); if (item == null) return null; item.IsCompleted = input.IsCompleted; item.CompletedBy = input.IsCompleted ? input.CompletedBy : null; item.CompletedAt = input.IsCompleted ? DateTime.UtcNow : null; await _dispatchDataService.UpdateChecklistItemAsync(item); return new ChecklistItemUpdateResult { Message = "Updated", Id = item.Id, IsCompleted = item.IsCompleted, CompletedBy = item.CompletedBy, CompletedAt = item.CompletedAt }; } public async Task AddChecklistItemAsync(AddChecklistItemInput input) { var checklistItems = await _dispatchDataService.GetChecklistItemsAsync(input.DispatchId); var maxSort = checklistItems.Any() ? checklistItems.Max(c => c.SortOrder) : -1; var item = new DispatchChecklistItem { DispatchId = input.DispatchId, WorkOrderId = input.WorkOrderId, ItemText = input.ItemText, SortOrder = maxSort + 1, IsCompleted = false }; await _dispatchDataService.AddChecklistItemAsync(item); return new AddChecklistItemResult { Id = item.Id, ItemText = item.ItemText, SortOrder = item.SortOrder, IsCompleted = item.IsCompleted }; } } }