2026-07-24 11:23:51 -03:00
using Data.SeaHavenIndustries ;
using Data.SeaHavenIndustries.Enums ;
2026-07-24 12:07:48 -03:00
using Microsoft.Extensions.Options ;
2026-07-24 11:23:51 -03:00
using SeaHaven.DataServices.Interfaces ;
using SeaHaven.DataServices.Models ;
2026-07-24 12:07:48 -03:00
using SeaHaven.Services.Configuration ;
2026-07-24 11:23:51 -03:00
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 ;
2026-07-24 12:07:48 -03:00
private readonly FrontendOptions _frontendOptions ;
2026-07-24 11:23:51 -03:00
public WorkOrderDispatchService (
IWorkOrderDataService workOrderDataService ,
IDispatchDataService dispatchDataService ,
IVendorDataService vendorDataService ,
IUserDataService userDataService ,
ITaskListTemplateDataService taskListTemplateDataService ,
ICommentDataService commentDataService ,
IDispatchEmailPort emailPort ,
IVendorTokenPort vendorTokenPort ,
2026-07-24 12:07:48 -03:00
IOptions < FrontendOptions > frontendOptions )
2026-07-24 11:23:51 -03:00
{
_workOrderDataService = workOrderDataService ;
_dispatchDataService = dispatchDataService ;
_vendorDataService = vendorDataService ;
_userDataService = userDataService ;
_taskListTemplateDataService = taskListTemplateDataService ;
_commentDataService = commentDataService ;
_emailPort = emailPort ;
_vendorTokenPort = vendorTokenPort ;
2026-07-24 12:07:48 -03:00
_frontendOptions = frontendOptions . Value ;
2026-07-24 11:23:51 -03:00
}
public async Task < DispatchToVendorResult > DispatchToVendorAsync ( DispatchToVendorInput input )
{
2026-07-24 12:07:48 -03:00
var workOrders = await _workOrderDataService . GetByRequestedIdsWithDetailsAsync (
input . WorkOrderIds ,
CancellationToken . None ) ;
2026-07-24 11:23:51 -03:00
if ( workOrders . Count = = 0 )
throw new InvalidOperationException ( "No work orders found" ) ;
2026-07-24 12:07:48 -03:00
var primaryWO = workOrders [ 0 ] ;
2026-07-24 11:23:51 -03:00
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 ( ) : "" ;
2026-07-24 12:07:48 -03:00
var templateItemTexts = await LoadTemplateItemTextsAsync ( input . TaskListTemplateId ) ;
var distinctVendorIds = input . VendorIds . Distinct ( ) . ToList ( ) ;
var vendors = await _vendorDataService . GetByIdsOrderedAsync ( distinctVendorIds , CancellationToken . None ) ;
2026-07-24 11:23:51 -03:00
var dispatches = new List < DispatchResultItem > ( ) ;
2026-07-24 12:07:48 -03:00
foreach ( var vendor in vendors )
2026-07-24 11:23:51 -03:00
{
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 ,
2026-07-24 12:07:48 -03:00
VendorId = vendor . Id ,
2026-07-24 11:23:51 -03:00
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 ( )
} ;
2026-07-24 12:07:48 -03:00
var checklistItems = BuildChecklistItems ( templateItemTexts , input . CustomChecklistItems ) ;
2026-07-24 11:23:51 -03:00
dispatch . ChecklistItems = checklistItems ;
var woListHtml = string . Join ( "" , workOrders . Select ( wo = >
$"<tr><td style='padding:6px 8px;border:1px solid #ddd;'>{System.Net.WebUtility.HtmlEncode(wo.InternalWONumber ?? "")}</td>" +
$"<td style='padding:6px 8px;border:1px solid #ddd;'>{System.Net.WebUtility.HtmlEncode(wo.WorkerOrderTitle ?? "")}</td>" +
$"<td style='padding:6px 8px;border:1px solid #ddd;'>{System.Net.WebUtility.HtmlEncode(wo.Priority ?? "")}</td>" +
$"<td style='padding:6px 8px;border:1px solid #ddd;'>{wo.DueDate?.ToString(" yyyy - MM - dd ") ?? " N / A "}</td></tr>" ) ) ;
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 ) ;
2026-07-24 12:07:48 -03:00
var vendorToken = await _vendorTokenPort . GetOrCreateActiveTokenAsync ( vendor . Id ) ;
var frontendBase = _frontendOptions . FrontendBaseUrl ? . TrimEnd ( '/' ) ? ? "" ;
2026-07-24 11:23:51 -03:00
var portalUrl = $"{frontendBase}/v/{vendorToken}/dashboard" ;
var html = $ @ "
< h2 > Work Order Dispatch — { dispatchNumber } < / h2 >
< p > < strong > Location : < / strong > { locationName } { locationAddr } < / p >
< p > < strong > Vendor PO : < / strong > { poNumber } & nbsp ; < strong > NTE : < / strong > $ { input . NTEAmount ? . ToString ( "F2" ) ? ? "N/A" } < / p >
< h3 > Work Orders < / h3 >
< table style = ' border - collapse : collapse ; width : 100 % ; font - family : Arial , sans - serif ; ' >
< tr style = ' background : # f5f5f5 ; ' >
< th style = ' padding : 6 px 8 px ; border : 1 px solid # ddd ; text - align : left ; ' > WO # < / th >
< th style = ' padding : 6 px 8 px ; border : 1 px solid # ddd ; text - align : left ; ' > Title < / th >
< th style = ' padding : 6 px 8 px ; border : 1 px solid # ddd ; text - align : left ; ' > Priority < / th >
< th style = ' padding : 6 px 8 px ; border : 1 px solid # ddd ; text - align : left ; ' > Due Date < / th >
< / tr >
{ woListHtml }
< / table >
< h3 > Description < / h3 >
< p > { descriptionHtml } < / p >
< br / >
< div style = ' text - align : center ; margin : 20 px 0 ; ' >
< a href = ' { portalUrl } ' style = ' display : inline - block ; padding : 12 px 32 px ; background : # 28 a745 ; color : white ; text - decoration : none ; border - radius : 6 px ; font - weight : bold ; font - size : 16 px ; ' > View Work Order < / a >
< / div >
< br / > < p style = ' color : # 666 ; font - size : 13 px ; ' > { senderNameHtml } < br / > Sea Haven Industries < / p >
< hr / > < p style = ' color : # 999 ; font - size : 12 px ; ' > Reply to this email to communicate about this dispatch . < / p > ";
var emailSent = await _emailPort . SendDispatchEmailAsync ( vendor . Email ! , subject , html , replyTo ) ;
dispatch . EmailSent = emailSent ;
var savedDispatch = await _dispatchDataService . AddAsync ( dispatch ) ;
2026-07-24 12:07:48 -03:00
var auditLogs = workOrders . Select ( wo = > new WorkOrderAuditLog
2026-07-24 11:23:51 -03:00
{
2026-07-24 12:07:48 -03:00
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 ) ;
2026-07-24 11:23:51 -03:00
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 } ;
}
2026-07-24 12:07:48 -03:00
private async Task < List < string? > ? > 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 < DispatchChecklistItem > BuildChecklistItems ( List < string? > ? templateItemTexts , List < string > ? customChecklistItems )
{
var checklistItems = new List < DispatchChecklistItem > ( ) ;
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 ;
}
2026-07-24 11:23:51 -03:00
public async Task < IEnumerable < DispatchReadModel > > 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 < DispatchDetailReadModel ? > GetDispatchDetailAsync ( int id )
{
return await _dispatchDataService . GetDispatchDetailAsync ( id ) ;
}
public async Task < bool > 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 < AddDispatchCommentResult ? > 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 = $"<p>{input.Text}</p><br/><p style='color:#666;font-size:13px;'>{senderName}<br/>Sea Haven Industries</p><hr/><p style='color:#999;font-size:12px;'>Reply to this email to respond.</p>" ;
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 < VerifyDispatchResult ? > 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 < string > ( ) ;
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 < AddDispatchSignoffResult ? > 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 < ChecklistItemUpdateResult ? > 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 < AddChecklistItemResult > 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
} ;
}
}
}