using System.Security.Claims; using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using FluentValidation; using SeaHaven.DataServices.Interfaces; using SeaHaven.DataServices.Models; using SeaHaven.Services.DTOs; using SeaHaven.Services.Exceptions; using SeaHaven.Services.Interfaces; using SeaHaven.Services.Validation; namespace SeaHaven.Services.Implementation { public class WorkOrderService : IWorkOrderService { private readonly IWorkOrderDataService _workOrderDataService; private readonly ICommentDataService _commentDataService; private readonly IUserDataService _userDataService; private readonly IQuotesDataService _quotesDataService; private readonly IFileStoragePort _fileStorage; private readonly ICreateWorkOrderValidation _createValidator; private readonly IUpdateWorkOrderValidation _updateValidator; private readonly IWorkOrderAccountResolver _accountResolver; public WorkOrderService( IWorkOrderDataService workOrderDataService, ICommentDataService commentDataService, IUserDataService userDataService, IQuotesDataService quotesDataService, IFileStoragePort fileStorage, ICreateWorkOrderValidation createValidator, IUpdateWorkOrderValidation updateValidator, IWorkOrderAccountResolver accountResolver) { _workOrderDataService = workOrderDataService; _commentDataService = commentDataService; _userDataService = userDataService; _quotesDataService = quotesDataService; _fileStorage = fileStorage; _createValidator = createValidator; _updateValidator = updateValidator; _accountResolver = accountResolver; } public async Task GetWorkOrderByIdAsync(int id) { var workOrder = await _workOrderDataService.GetByIdAsync(id); return workOrder == null ? null : WorkOrderDTO.MapToModel(workOrder); } public async Task GetWorkOrderByIdWithDetailsAsync(int id) { var workOrder = await _workOrderDataService.GetByIdWithDetailsAsync(id); return workOrder == null ? null : WorkOrderDTO.MapToModel(workOrder); } public async Task> GetAllWorkOrdersAsync() { var workOrders = await _workOrderDataService.GetAllAsync(); return workOrders.Select(WorkOrderDTO.MapToModel); } public async Task> GetWorkOrdersPagedAsync(int page, int pageSize, string? search = null, string? status = null, string? priority = null) { var (items, totalCount) = await _workOrderDataService.GetPagedAsync(page, pageSize, search, status, priority); return new PagedResult { Items = items.Select(WorkOrderDTO.MapToModel), TotalCount = totalCount, Page = page, PageSize = pageSize }; } public async Task CreateWorkOrderAsync(CreateWorkOrderDTO dto, ClaimsPrincipal user, string userId) { var validationResult = await _createValidator.ValidateAsync(dto); if (!validationResult.IsValid) throw new ValidationException(validationResult.Errors); var accountId = await _accountResolver.ResolveForAuthenticatedCreateAsync( user, dto.Customer, CancellationToken.None); var workOrder = CreateWorkOrderDTO.MapToEntity(dto); workOrder.AccountId = accountId; workOrder.createdby = userId; workOrder.CreatedDate = DateTime.UtcNow; workOrder.InternalWONumber = await _workOrderDataService.GenerateNextInternalWONumberAsync(); var saved = await _workOrderDataService.AddAsync(workOrder); await LogAuditAsync(saved.Id, userId, AuditAction.Create, "Work order created"); return saved.Id; } public async Task UpdateWorkOrderAsync(int id, UpdateWorkOrderDTO dto, string userId) { var validationResult = await _updateValidator.ValidateAsync(dto); if (!validationResult.IsValid) throw new ValidationException(validationResult.Errors); var existingWorkOrder = await _workOrderDataService.GetByIdAsync(id); if (existingWorkOrder == null) return false; var changes = new List(); changes.AddRange(TrackChanges(existingWorkOrder, dto)); UpdateWorkOrderDTO.UpdateEntity(dto, existingWorkOrder); await _workOrderDataService.UpdateAsync(existingWorkOrder); if (changes.Any()) await LogAuditAsync(id, userId, AuditAction.Edit, string.Join(", ", changes)); return true; } public async Task DeleteWorkOrderAsync(int id, string userId) { if (!await _workOrderDataService.ExistsAsync(id)) return false; await _workOrderDataService.DeleteWorkOrderAttachmentsAsync(id); await _workOrderDataService.DeleteWorkOrderContactsAsync(id); await _workOrderDataService.DeleteWorkOrderCategoriesAsync(id); await _workOrderDataService.DeleteAsync(id); await LogAuditAsync(id, userId, AuditAction.Delete, "Work order deleted"); return true; } public async Task WorkOrderExistsAsync(int id) => await _workOrderDataService.ExistsAsync(id); public async Task GenerateNextInternalWONumberAsync() => await _workOrderDataService.GenerateNextInternalWONumberAsync(); public async Task GetTotalWorkOrderCountAsync() => await _workOrderDataService.CountAsync(); public async Task<(IEnumerable Items, int TotalCount)> GetWorkOrderListPagedAsync( int page, int pageSize, string? search, string? status, string? assignee, int? locationId, string? sortBy, string? sortDir, int? accountId = null) { return await _workOrderDataService.GetWorkOrderListPagedAsync( page, pageSize, search, status, assignee, locationId, sortBy, sortDir, accountId); } public async Task GetWorkOrderDetailAsync(int id, int? accountId = null) { return await _workOrderDataService.GetWorkOrderDetailAsync(id, accountId); } public async Task GetFilteredWorkOrdersAsync( string[] assignto, int[]? location, string[]? priority, string[]? status, string[]? duedate, string search, string sort, string sortby, int page, int pageSize, int? accountId = null) { var allWorkOrders = await _workOrderDataService.GetNonTemplateWorkOrdersWithLocationsAsync(accountId); var data = allWorkOrders.AsEnumerable(); if (!string.IsNullOrEmpty(search)) { var s = search.ToLower(); data = data.Where(w => (w.WorkerOrderTitle ?? "").ToLower().Contains(s) || (w.WorkerOrderNumber ?? "").ToLower().Contains(s) || (w.Locations != null && (w.Locations.Title ?? "").ToLower().Contains(s))); } if (assignto != null && assignto.Length > 0 && assignto[0] != "" && assignto[0] != null) data = data.Where(w => assignto.Contains(w.AssignTo)); if (location != null && location.Length > 0 && location[0] != 0) data = data.Where(w => location.Contains(w.LocationId ?? 0)); if (priority != null && priority.Length > 0 && priority[0] != "" && priority[0] != null) data = data.Where(w => priority.Contains(w.Priority)); if (status != null && status.Length > 0 && status[0] != "" && status[0] != null) data = data.Where(w => status.Contains(w.Status)); var dataList = data.ToList(); dataList = dataList.Where(w => duedate == null || duedate.Length == 0 || duedate[0] == null || duedate.Any(dd => w.DueDate.HasValue && CheckDueDate(dd, w.DueDate.Value.Date, DateTime.UtcNow.Date))).ToList(); if (!string.IsNullOrEmpty(sort) && !string.IsNullOrEmpty(sortby) && sortby != "") { switch (sortby) { case "Name": dataList = sort.ToLower() == "ascending" ? dataList.OrderBy(w => w.WorkerOrderTitle).ToList() : dataList.OrderByDescending(w => w.WorkerOrderTitle).ToList(); break; case "Due Date": dataList = sort.ToLower() == "ascending" ? dataList.OrderBy(w => w.DueDate).ToList() : dataList.OrderByDescending(w => w.DueDate).ToList(); break; case "Severity": dataList = sort.ToLower() == "ascending" ? dataList.OrderBy(w => w.Priority).ToList() : dataList.OrderByDescending(w => w.Priority).ToList(); break; } } var totalCount = dataList.Count; var pagedData = dataList .Skip((page - 1) * pageSize) .Take(pageSize) .Select(s => new FilteredWorkOrderReadModel { Id = s.Id, WorkerOrderTitle = s.WorkerOrderTitle, DueDate = s.DueDate, Status = s.Status, Priority = s.Priority, Name = s.Locations != null ? s.Locations.Name : "" }) .OrderByDescending(d => d.Name) .ToList(); return new FilteredWorkOrderPageDTO { Data = pagedData, PageNumber = page, PageSize = pageSize, TotalCount = totalCount, TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize) }; } public async Task GetFilteredWorkOrders2Async( string[] assignto, int[]? location, string[]? priority, string[]? status, string[]? duedate, string search, string sort, string sortby, int page, int pageSize, int? accountId = null) { location ??= Array.Empty(); priority ??= Array.Empty(); status ??= Array.Empty(); duedate ??= Array.Empty(); search ??= string.Empty; var allWorkOrders = await _workOrderDataService.GetNonTemplateWorkOrdersWithLocationsAsync(accountId); var totalCount = allWorkOrders.Count; var currentDate = DateTime.UtcNow.Date; var data = allWorkOrders.Where(w => (((w.WorkerOrderTitle ?? "").Contains(search, StringComparison.OrdinalIgnoreCase) || (w.WorkerOrderNumber ?? "").Contains(search, StringComparison.OrdinalIgnoreCase) || (w.Locations != null && (w.Locations.Title ?? "").Contains(search, StringComparison.OrdinalIgnoreCase))) && (assignto.Length == 0 || assignto.Contains(w.AssignTo)) && (location.Length == 0 || (w.LocationId.HasValue && location.Contains(w.LocationId.Value))) && (priority.Length == 0 || priority.Contains(w.Priority)) && (status.Length == 0 || status.Contains(w.Status)))).ToList(); data = data.Where(w => duedate.Length == 0 || duedate.Any(dd => w.DueDate.HasValue && CheckDueDate(dd, w.DueDate.Value.Date, currentDate))).ToList(); if (!string.IsNullOrEmpty(sort) && !string.IsNullOrEmpty(sortby) && sortby != "") { switch (sortby) { case "Name": data = sort.ToLower() == "ascending" ? data.OrderBy(w => w.WorkerOrderTitle).ToList() : data.OrderByDescending(w => w.WorkerOrderTitle).ToList(); break; case "Due Date": data = sort.ToLower() == "ascending" ? data.OrderBy(w => w.DueDate).ToList() : data.OrderByDescending(w => w.DueDate).ToList(); break; case "Severity": data = sort.ToLower() == "ascending" ? data.OrderBy(w => w.Priority).ToList() : data.OrderByDescending(w => w.Priority).ToList(); break; } } var newdata = data .Select(s => new FilteredWorkOrderReadModel { Id = s.Id, WorkerOrderTitle = s.WorkerOrderTitle, DueDate = s.DueDate, Status = s.Status, Priority = s.Priority, Name = s.Locations != null ? s.Locations.Name : "" }) .OrderByDescending(d => d.Name) .ToList() .Skip((page - 1) * pageSize) .Take(pageSize) .ToList(); return new FilteredWorkOrderPageDTO { Data = newdata, PageNumber = page, PageSize = pageSize, TotalCount = totalCount, TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize) }; } public async Task ChangeStatusAsync(int id, string status, string userId) { var workOrder = await _workOrderDataService.GetByIdAsync(id); if (workOrder == null) return null; if (workOrder.Status != status) { await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog { WorkOrderId = id, UserId = userId, FieldName = "Status", OldValue = workOrder.Status ?? "", NewValue = status, Action = AuditAction.StatusChange.ToString(), CreatedAt = DateTime.UtcNow }); } workOrder.Status = status; await _workOrderDataService.UpdateAsync(workOrder); return WorkOrderDTO.MapToModel(workOrder); } public async Task ChangeAssignmentAsync(int id, string? assignTo, string userId) { var workOrder = await _workOrderDataService.GetByIdAsync(id); if (workOrder == null) return null; if (workOrder.AssignTo != assignTo) { var oldUser = workOrder.AssignTo != null ? await _userDataService.GetByIdAsync(workOrder.AssignTo) : null; var newUser = assignTo != null ? await _userDataService.GetByIdAsync(assignTo) : null; await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog { WorkOrderId = id, UserId = userId, FieldName = "AssignedTo", OldValue = oldUser != null ? (oldUser.FirstName + " " + oldUser.LastName).Trim() : "", NewValue = newUser != null ? (newUser.FirstName + " " + newUser.LastName).Trim() : "Unassigned", Action = AuditAction.AssignmentChange.ToString(), CreatedAt = DateTime.UtcNow }); } workOrder.AssignTo = assignTo; await _workOrderDataService.UpdateAsync(workOrder); var assignedUser = assignTo != null ? await _userDataService.GetByIdAsync(assignTo) : null; return new ChangeAssignmentResult { Message = "Updated Successfully", AssignedTo = assignedUser != null ? (assignedUser.FirstName + " " + assignedUser.LastName).Trim() : "" }; } public async Task CreateWorkOrderWithDetailsAsync( CreateWorkOrderWithDetailsInput input, ClaimsPrincipal user, string userId) { var createDto = new CreateWorkOrderDTO { WorkerOrderTitle = input.WorkerOrderTitle, Description = input.Description, Status = WorkOrderStatus.Open, Priority = Enum.TryParse(input.Priority, out var p) ? p : WorkOrderPriority.Medium, LocationId = input.LocationId, AssignTo = input.AssignTo, DueDate = input.DueDate, Customer = input.Customer }; var workOrderId = await CreateWorkOrderAsync(createDto, user, userId); if (input.ContactIds != null && input.ContactIds.Any()) await _workOrderDataService.AddWorkOrderContactsAsync(workOrderId, input.ContactIds); if (input.CategoryIds != null && input.CategoryIds.Any()) await _workOrderDataService.AddWorkOrderCategoriesAsync(workOrderId, input.CategoryIds); await SaveAttachmentsAndPhotos(workOrderId, input.Attachments, input.BeforPhotoAttachment, input.AfterPhotoAttachment, input.SignOffAttachment); return workOrderId; } public async Task UpdateWorkOrderWithDetailsAsync(UpdateWorkOrderWithDetailsInput input, string userId) { var updateDto = new UpdateWorkOrderDTO { WorkerOrderTitle = input.WorkerOrderTitle, Description = input.Description, Status = Enum.TryParse(input.Status, out var st) ? st : null, Priority = Enum.TryParse(input.Priority, out var pr) ? pr : null, LocationId = input.LocationId, AssignTo = input.AssignTo, DueDate = input.DueDate, CompletedDate = input.CompletedDate }; var success = await UpdateWorkOrderAsync(input.Id, updateDto, userId); if (!success) return false; await _workOrderDataService.DeleteWorkOrderContactsAsync(input.Id); await _workOrderDataService.DeleteWorkOrderCategoriesAsync(input.Id); if (input.ContactIds != null && input.ContactIds.Any()) await _workOrderDataService.AddWorkOrderContactsAsync(input.Id, input.ContactIds); if (input.CategoryIds != null && input.CategoryIds.Any()) await _workOrderDataService.AddWorkOrderCategoriesAsync(input.Id, input.CategoryIds); await SaveAttachmentsAndPhotos(input.Id, input.Attachments, input.BeforPhotoAttachment, input.AfterPhotoAttachment, input.SignOffAttachment); return true; } public async Task DeleteWorkOrderCascadeAsync(int id, string userId) { var success = await DeleteWorkOrderAsync(id, userId); if (!success) return false; await _quotesDataService.UnlinkFromWorkOrderAsync(id); var comments = await _commentDataService.GetByWorkOrderIdAsync(id); foreach (var comment in comments) await _commentDataService.DeleteAsync(comment.Id); return true; } public async Task AddCommentAsync(AddCommentInput input, ClaimsPrincipal user, string userId) { await EnsureWorkOrderInAccountScopeAsync(input.WorkorderId, user); var comment = new Comments { UserId = userId, WorkerOrderId = input.WorkorderId, Commenttext = input.Text, CommentType = input.CommentType ?? "vendor" }; if (input.Document != null) { var fileUrl = await _fileStorage.SaveFileAsync(input.Document); comment.Documents = fileUrl; } return await _commentDataService.AddAsync(comment); } public async Task AddCommentJsonAsync(AddCommentInput input, ClaimsPrincipal user, string userId) { await EnsureWorkOrderInAccountScopeAsync(input.WorkorderId, user); var comment = new Comments { UserId = userId, WorkerOrderId = input.WorkorderId, Commenttext = input.Text, CommentType = input.CommentType ?? "internal" }; var saved = await _commentDataService.AddAsync(comment); var displayUser = await _userDataService.GetByIdAsync(userId); return new CommentResult { Id = saved.Id, CreatedDate = saved.CreatedDate, Commenttext = saved.Commenttext, CommentType = saved.CommentType, UserName = displayUser != null ? (displayUser.FirstName + " " + displayUser.LastName).Trim() : "" }; } public async Task> GetCommentsAsync(ClaimsPrincipal user) { var accountId = _accountResolver.ResolveAccountFilter(user); var comments = await _commentDataService.GetAllForAccountAsync(accountId); return comments.Select(s => new CommentListItemReadModel { Id = s.Id, Commenttext = s.Commenttext, FirstName = s.ApplicationUser != null ? s.ApplicationUser.FirstName : "", Documents = s.Documents }).ToList(); } public async Task?> GetCommentsByWorkorderIdAsync( int woid, ClaimsPrincipal user) { var accountId = _accountResolver.ResolveAccountFilter(user); var workOrder = await _workOrderDataService.GetWorkOrderDetailAsync(woid, accountId); if (workOrder == null) return null; var comments = await _commentDataService.GetByWorkOrderIdAsync(woid); return comments.Select(s => new CommentListItemReadModel { Id = s.Id, Commenttext = s.Commenttext, FirstName = s.ApplicationUser != null ? s.ApplicationUser.FirstName : "", Documents = s.Documents }).ToList(); } private async Task EnsureWorkOrderInAccountScopeAsync(int workOrderId, ClaimsPrincipal user) { var accountId = _accountResolver.ResolveAccountFilter(user); var workOrder = await _workOrderDataService.GetWorkOrderDetailAsync(workOrderId, accountId); if (workOrder == null) throw new WorkOrderBoardValidationException("NotFound", "Work order not found."); } public async Task> GetWorkordersDDAsync(int? accountId = null) { var data = await _workOrderDataService.GetNonTemplateWorkOrdersWithLocationsAsync(accountId); return data; } public async Task> GetWorkordersAsync(int? accountId = null) { var workOrders = await _workOrderDataService.GetAllWithDetailsAsync(accountId); return workOrders.Select(s => new WorkorderFilterVM { Id = s.Id, Priority = s.Priority, Status = s.Status, WorkerOrderNumber = s.WorkerOrderNumber, WorkerOrderTitle = s.WorkerOrderTitle, DueDate = s.DueDate, AssignToUserName = s.AssignToUser != null ? s.AssignToUser.FirstName : "", LocationName = s.Locations != null ? s.Locations.Name : "", LocationId = s.LocationId, PO = s.PO, TT = s.TT }).ToList(); } private async Task SaveAttachmentsAndPhotos(int workOrderId, List? attachments, Microsoft.AspNetCore.Http.IFormFile? beforPhoto, Microsoft.AspNetCore.Http.IFormFile? afterPhoto, Microsoft.AspNetCore.Http.IFormFile? signOffPhoto) { if (attachments != null) { foreach (var attachment in attachments) { var fileUrl = await _fileStorage.SaveFileAsync(attachment); await _workOrderDataService.AddWorkOrderAttachmentAsync(new WorkOrderAttachments { WorkorderId = workOrderId, Attachments = fileUrl }); } } if (beforPhoto != null) { var fileUrl = await _fileStorage.SaveFileAsync(beforPhoto); var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId); if (workOrder != null) { workOrder.BeforPhotoAttachment = fileUrl; await _workOrderDataService.UpdateAsync(workOrder); } } if (afterPhoto != null) { var fileUrl = await _fileStorage.SaveFileAsync(afterPhoto); var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId); if (workOrder != null) { workOrder.AfterPhotoAttachment = fileUrl; await _workOrderDataService.UpdateAsync(workOrder); } } if (signOffPhoto != null) { var fileUrl = await _fileStorage.SaveFileAsync(signOffPhoto); var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId); if (workOrder != null) { workOrder.SignOffAttachment = fileUrl; await _workOrderDataService.UpdateAsync(workOrder); } } } private static bool CheckDueDate(string dd, DateTime dueDate, DateTime currentDate) { return dd switch { "Today" => dueDate == currentDate, "Tomorrow" => dueDate == currentDate.AddDays(1), "Next Week" => dueDate >= currentDate.AddDays(1) && dueDate <= currentDate.AddDays(7), "Next Month" => dueDate >= currentDate.AddDays(1) && dueDate <= currentDate.AddMonths(1), "Over Due" => dueDate < currentDate, _ => false }; } private List TrackChanges(WorkOrder existing, UpdateWorkOrderDTO dto) { var changes = new List(); if (dto.WorkerOrderTitle != null && dto.WorkerOrderTitle != existing.WorkerOrderTitle) changes.Add($"Title: {existing.WorkerOrderTitle} → {dto.WorkerOrderTitle}"); if (dto.Status.HasValue && dto.Status.ToString() != existing.Status) changes.Add($"Status: {existing.Status} → {dto.Status}"); if (dto.Priority.HasValue && dto.Priority.ToString() != existing.Priority) changes.Add($"Priority: {existing.Priority} → {dto.Priority}"); if (dto.AssignTo != null && dto.AssignTo != existing.AssignTo) changes.Add($"Assigned To: {existing.AssignTo} → {dto.AssignTo}"); if (dto.DueDate.HasValue && dto.DueDate != existing.DueDate) changes.Add($"Due Date: {existing.DueDate:yyyy-MM-dd} → {dto.DueDate:yyyy-MM-dd}"); return changes; } private async Task LogAuditAsync(int workOrderId, string userId, AuditAction action, string description) { await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog { WorkOrderId = workOrderId, Action = action.ToString(), FieldName = description, UserId = userId, CreatedAt = DateTime.UtcNow }); } } }