shoc-backend/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs
Alexandre Brandizzi a9dee0bb84 merge: integrate origin/dev into work-orders-phase-4
Merge origin/dev (vendor roadmap PRs #28/#29, phases 1-3 PRs #13-15)
into work-orders-phase-4. Conflict in WorkOrderController.cs resolved by
keeping both the Phase 4 board/search endpoint and the dev-side XML doc
comment on GetDispatcherLookups (both additions at the same site).

Migration timestamps remain monotonic and non-overlapping. Phase 4 global
advanced search, search index migrations, and the cross-platform LocalDB
test guard are preserved alongside the merged vendor roadmap and phases 1-3.

Validation (Docker .NET 8 SDK): build 0 errors; 191 tests pass across
all three test projects (64 + 18 + 109).
2026-07-24 13:38:20 -03:00

1580 lines
71 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Enums;
using FluentValidation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.VisualBasic;
using SendGrid.Helpers.Mail;
using System.IO;
using System.Net.Http.Headers;
using System.Security.Claims;
using static System.Runtime.InteropServices.JavaScript.JSType;
using SeaHaven.Services.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Exceptions;
using SeaHaven.Services.Helpers;
using SeaHaven.DataServices.Interfaces;
namespace Api.SeaHavenIndustries.Controllers
{
[Authorize]
[ApiController]
[Route("api/WorkOrder")]
[Route("api/workorders")]
public class WorkOrderController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly IWorkOrderService _workOrderService;
private readonly IWorkOrderDataService _workOrderDataService;
private readonly ICommentDataService _commentDataService;
private readonly IQuotesDataService _quotesDataService;
private readonly IUserDataService _userDataService;
private readonly IDispatchDataService _dispatchDataService;
private readonly IVendorDataService _vendorDataService;
private readonly ITaskListTemplateDataService _taskListTemplateDataService;
private readonly ApplicationDbContext _db;
private readonly IWebHostEnvironment _webHostEnvironment;
IHttpContextAccessor _httpContext;
private readonly Helper.SendMessage _sendMessage;
private readonly Helper.VendorPortalTokenService _vendorTokens;
private readonly IConfiguration _config;
private readonly IWorkOrderBoardService _workOrderBoardService;
private readonly IWorkOrderBoardUpdateService _boardUpdateService;
private readonly IWorkOrderBoardCreateService _boardCreateService;
private readonly IWorkOrderBoardCancelService _boardCancelService;
private readonly IWorkOrderAdvancedSearchService _advancedSearchService;
public WorkOrderController(
UserManager<ApplicationUser> userManager,
IWorkOrderService workOrderService,
IWorkOrderDataService workOrderDataService,
ICommentDataService commentDataService,
IQuotesDataService quotesDataService,
IUserDataService userDataService,
IDispatchDataService dispatchDataService,
IVendorDataService vendorDataService,
ITaskListTemplateDataService taskListTemplateDataService,
ApplicationDbContext db,
IWebHostEnvironment webHostEnvironment,
IHttpContextAccessor httpContext,
Helper.SendMessage sendMessage,
Helper.VendorPortalTokenService vendorTokens,
IConfiguration config,
IWorkOrderBoardService workOrderBoardService,
IWorkOrderBoardUpdateService boardUpdateService,
IWorkOrderBoardCreateService boardCreateService,
IWorkOrderBoardCancelService boardCancelService,
IWorkOrderAdvancedSearchService advancedSearchService)
{
_userManager = userManager;
_workOrderService = workOrderService;
_workOrderDataService = workOrderDataService;
_commentDataService = commentDataService;
_quotesDataService = quotesDataService;
_userDataService = userDataService;
_dispatchDataService = dispatchDataService;
_vendorDataService = vendorDataService;
_taskListTemplateDataService = taskListTemplateDataService;
_db = db;
_webHostEnvironment = webHostEnvironment;
_httpContext = httpContext;
_sendMessage = sendMessage;
_vendorTokens = vendorTokens;
_config = config;
_workOrderBoardService = workOrderBoardService;
_boardUpdateService = boardUpdateService;
_boardCreateService = boardCreateService;
_boardCancelService = boardCancelService;
_advancedSearchService = advancedSearchService;
}
/// <summary>
/// Weekly board payload.
/// When <paramref name="myWorkOrders"/> is true, the authenticated user filter wins and
/// <paramref name="dispatchers"/> is ignored (not AND-combined).
/// Weekend WOs in a Mon–Sun window may have <c>dayGroup = null</c>; FE should handle that.
/// </summary>
[HttpGet("board")]
public async Task<IActionResult> GetBoard(
[FromQuery, BindRequired] DateOnly weekStart,
[FromQuery] DateOnly? weekEnd = null,
[FromQuery] List<string>? dispatchers = null,
[FromQuery] bool myWorkOrders = false,
[FromQuery] List<WorkOrderType>? types = null,
[FromQuery] string? search = null)
{
var resolvedWeekEnd = weekEnd ?? weekStart.AddDays(4);
var weekValidationError = WorkOrderOperationalWeek.ValidateWeekWindow(weekStart, resolvedWeekEnd);
if (weekValidationError != null)
return BadRequest(weekValidationError);
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var query = new WorkOrderBoardQueryDto
{
WeekStart = weekStart,
WeekEnd = weekEnd,
Dispatchers = dispatchers,
MyWorkOrders = myWorkOrders,
Types = types,
Search = search
};
var result = await _workOrderBoardService.GetBoardAsync(query, userId);
return Ok(result);
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
}
[HttpGet("board/search")]
public async Task<IActionResult> SearchBoard([FromQuery] WorkOrderAdvancedSearchQueryDto query)
{
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var result = await _advancedSearchService.SearchAsync(query, userId);
return Ok(result);
}
catch (ArgumentException ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
/// <summary>
/// Dispatcher dropdown options. Currently returns all non-deleted users
/// (role-filtered assignee list deferred until roles settle).
/// </summary>
[HttpGet("lookups/dispatchers")]
public async Task<IActionResult> GetDispatcherLookups()
{
var result = await _workOrderBoardService.GetDispatcherLookupsAsync();
return Ok(result);
}
[HttpPatch("{id:int}/board")]
public async Task<IActionResult> PatchBoardField(int id, [FromBody] WorkOrderBoardPatchRequestDto request)
{
try
{
var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var row = await _boardUpdateService.PatchFieldAsync(id, request, actorId);
return Ok(row);
}
catch (WorkOrderBoardConcurrencyException ex)
{
return Conflict(new WorkOrderBoardConflictDto
{
CurrentState = ex.CurrentState
});
}
catch (WorkOrderBoardValidationException ex) when (ex.Code == "NotFound")
{
return NotFound(new WorkOrderBoardValidationErrorDto
{
Code = ex.Code,
Message = ex.Message
});
}
catch (WorkOrderBoardValidationException ex)
{
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto
{
Code = ex.Code,
Message = ex.Message
});
}
catch (ArgumentException ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPost("board")]
public async Task<IActionResult> CreateBoardWorkOrder([FromBody] WorkOrderBoardCreateRequestDto request)
{
try
{
var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var row = await _boardCreateService.CreateAsync(request, actorId);
return Ok(row);
}
catch (ValidationException vex)
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (WorkOrderBoardValidationException ex) when (ex.Code == "DuplicateWoNumber")
{
return Conflict(new WorkOrderBoardValidationErrorDto
{
Code = ex.Code,
Message = ex.Message
});
}
catch (WorkOrderBoardValidationException ex) when (ex.Code == "NotFound")
{
return NotFound(new WorkOrderBoardValidationErrorDto
{
Code = ex.Code,
Message = ex.Message
});
}
catch (WorkOrderBoardValidationException ex)
{
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto
{
Code = ex.Code,
Message = ex.Message
});
}
}
[HttpPost("{id:int}/cancel")]
public async Task<IActionResult> CancelBoardWorkOrder(int id)
{
try
{
var actorId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var row = await _boardCancelService.CancelAsync(id, actorId);
return Ok(row);
}
catch (WorkOrderBoardValidationException ex) when (ex.Code == "NotFound")
{
return NotFound(new WorkOrderBoardValidationErrorDto
{
Code = ex.Code,
Message = ex.Message
});
}
catch (WorkOrderBoardValidationException ex)
{
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto
{
Code = ex.Code,
Message = ex.Message
});
}
}
[HttpPost]
[Route("AddWorkorder")]
public async Task<IActionResult> Addworkorder([FromForm] Workorder_DTO model)
{
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var createDto = new CreateWorkOrderDTO
{
WorkerOrderTitle = model.WorkerOrderTitle ?? "",
Description = model.Description,
Status = WorkOrderStatus.Open,
Priority = model.Priority ?? WorkOrderPriority.Medium,
LocationId = model.LocationId,
AssignTo = model.AssignTo,
DueDate = model.DueDate
};
var workOrderId = await _workOrderService.CreateWorkOrderAsync(createDto, userId!);
if (model.ContactIds != null && model.ContactIds.Any())
{
await _workOrderDataService.AddWorkOrderContactsAsync(workOrderId, model.ContactIds);
}
if (model.CategoryIds != null && model.CategoryIds.Any())
{
await _workOrderDataService.AddWorkOrderCategoriesAsync(workOrderId, model.CategoryIds);
}
if (model.Attachments != null)
{
foreach (var attachment in model.Attachments)
{
var fileUrl = await SaveFileAsync(attachment);
await _workOrderDataService.AddWorkOrderAttachmentAsync(new WorkOrderAttachments
{
WorkorderId = workOrderId,
Attachments = fileUrl
});
}
}
if (model.BeforPhotoAttachment != null)
{
var fileUrl = await SaveFileAsync(model.BeforPhotoAttachment);
var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId);
if (workOrder != null)
{
workOrder.BeforPhotoAttachment = fileUrl;
await _workOrderDataService.UpdateAsync(workOrder);
}
}
if (model.AfterPhotoAttachment != null)
{
var fileUrl = await SaveFileAsync(model.AfterPhotoAttachment);
var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId);
if (workOrder != null)
{
workOrder.AfterPhotoAttachment = fileUrl;
await _workOrderDataService.UpdateAsync(workOrder);
}
}
if (model.SignOffAttachment != null)
{
var fileUrl = await SaveFileAsync(model.SignOffAttachment);
var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId);
if (workOrder != null)
{
workOrder.SignOffAttachment = fileUrl;
await _workOrderDataService.UpdateAsync(workOrder);
}
}
return Ok(new DataResponse { Message = "Work order created successfully", Status = "200", Data = workOrderId });
}
catch (ValidationException vex)
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPost]
[HttpPut("{id:int}")]
[Route("EditWorkorder")]
public async Task<IActionResult> Editworkorder([FromForm] EditWorkorder_DTO model)
{
try
{
if (!model.Id.HasValue)
return BadRequest(new Response { Status = "Error", Message = "Work order ID is required" });
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var updateDto = new UpdateWorkOrderDTO
{
WorkerOrderTitle = model.WorkerOrderTitle,
Description = model.Description,
Status = model.Status,
Priority = model.Priority,
LocationId = model.LocationId,
AssignTo = model.AssignTo,
DueDate = model.DueDate,
CompletedDate = model.CompletedDate
};
var success = await _workOrderService.UpdateWorkOrderAsync(model.Id.Value, updateDto, userId!);
if (!success)
return BadRequest(new Response { Status = "Error", Message = "Work order not found" });
await _workOrderDataService.DeleteWorkOrderContactsAsync(model.Id.Value);
await _workOrderDataService.DeleteWorkOrderCategoriesAsync(model.Id.Value);
if (model.ContactIds != null && model.ContactIds.Any())
{
await _workOrderDataService.AddWorkOrderContactsAsync(model.Id.Value, model.ContactIds);
}
if (model.CategoryIds != null && model.CategoryIds.Any())
{
await _workOrderDataService.AddWorkOrderCategoriesAsync(model.Id.Value, model.CategoryIds);
}
if (model.Attachments != null)
{
foreach (var attachment in model.Attachments)
{
var fileUrl = await SaveFileAsync(attachment);
await _workOrderDataService.AddWorkOrderAttachmentAsync(new WorkOrderAttachments
{
WorkorderId = model.Id.Value,
Attachments = fileUrl
});
}
}
if (model.BeforPhotoAttachment != null)
{
var fileUrl = await SaveFileAsync(model.BeforPhotoAttachment);
var workOrder = await _workOrderDataService.GetByIdAsync(model.Id.Value);
if (workOrder != null)
{
workOrder.BeforPhotoAttachment = fileUrl;
await _workOrderDataService.UpdateAsync(workOrder);
}
}
if (model.AfterPhotoAttachment != null)
{
var fileUrl = await SaveFileAsync(model.AfterPhotoAttachment);
var workOrder = await _workOrderDataService.GetByIdAsync(model.Id.Value);
if (workOrder != null)
{
workOrder.AfterPhotoAttachment = fileUrl;
await _workOrderDataService.UpdateAsync(workOrder);
}
}
if (model.SignOffAttachment != null)
{
var fileUrl = await SaveFileAsync(model.SignOffAttachment);
var workOrder = await _workOrderDataService.GetByIdAsync(model.Id.Value);
if (workOrder != null)
{
workOrder.SignOffAttachment = fileUrl;
await _workOrderDataService.UpdateAsync(workOrder);
}
}
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
}
catch (ValidationException vex)
{
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
return BadRequest(new Response { Status = "Validation Error", Message = errors });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
// -----------------------------------
// GET: api/WorkOrder/GetWorkOrderList
// -----------------------------------
[HttpGet("GetWorkOrderList")]
public async Task<IActionResult> GetWorkOrderList(string? search = "", int page = 1, int pageSize = 12, string? sortBy = "lastUpdated", string? sortDir = "desc", string? status = "", string? assignee = "", int? locationId = null)
{
var (data, totalCount) = await _workOrderDataService.GetWorkOrderListPagedAsync(
page,
pageSize,
search,
status,
assignee,
locationId,
sortBy,
sortDir);
var viewModel = new Pagination_DTO
{
Data = data,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
};
return Ok(viewModel);
}
[Authorize(Roles = "Admin")]
[HttpDelete]
[Route("DeleteWorkorder")]
public async Task<IActionResult> Deleteworkorder([FromBody] EditWorkorder_DTO model)
{
int id = model.Id.HasValue ? model.Id.Value : 0;
if (id == 0)
{
return BadRequest("Id is required");
}
try
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var success = await _workOrderService.DeleteWorkOrderAsync(id, userId!);
if (!success)
return BadRequest("Work order not found!");
await _quotesDataService.UnlinkFromWorkOrderAsync(id);
var comments = await _commentDataService.GetByWorkOrderIdAsync(id);
foreach (var comment in comments)
{
await _commentDataService.DeleteAsync(comment.Id);
}
return Ok(new DataResponse { Message = "Removed Successfully", Status = "200" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpGet]
[Route("GetFilteredWorkorder")]
public IActionResult GetFilteredWorkorder(
[FromQuery] string[] assignto,
[FromQuery] int[]? location,
[FromQuery] string[]? priority,
[FromQuery] string[]? status,
[FromQuery] string[]? duedate,
[FromQuery] string search = "",
[FromQuery] string sort = "Sort Order",
[FromQuery] string sortby = "Sort By",
int page = 1,
int pageSize = 10)
{
try
{
IQueryable<WorkOrder> query = _db.workOrders.Where(w => w.istemplate != true).Include(w => w.Locations);
if (!string.IsNullOrEmpty(search))
{
var s = search.ToLower();
query = query.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)
{
if (assignto[0] != "" && assignto[0] != null)
query = query.Where(w => assignto.Contains(w.AssignTo));
}
if (location != null && location.Length > 0)
{
if (location[0] != 0)
query = query.Where(w => location.Contains(w.LocationId ?? 0));
}
if (priority != null && priority.Length > 0)
{
if (priority[0] != "" && priority[0] != null)
query = query.Where(w => priority.Contains(w.Priority));
}
if (status != null && status.Length > 0)
{
if (status[0] != "" && status[0] != null)
query = query.Where(w => status.Contains(w.Status));
}
var data = query.ToList();
// Perform client-side filtering for due dates
data = data.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();
// Apply sorting
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;
default:
// Handle default case or throw an exception
break;
}
}
var totalCount = data.Count();
var pagedData = data
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(s => new { s.Id, s.WorkerOrderTitle, s.DueDate, s.Status, s.Priority, Name = s.Locations != null ? s.Locations.Name : "" })
.OrderByDescending(d => d.Name)
.ToList();
var totalPages1 = (int)Math.Ceiling(totalCount / (double)pageSize);
// Create a view model containing the paged items and pagination metadata
var viewModel = new Pagination_DTO
{
Data = pagedData,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = totalPages1
};
return Ok(viewModel);
}
catch (Exception ex)
{
// Handle exception and return a default result or an error response
return StatusCode(500, "Internal Server Error");
}
}
[HttpGet("{id:int}")]
[HttpGet("GetWorkorderById")]
public async Task<IActionResult> GetWorkorderById(int id)
{
var exist = await _db.workOrders.Include(w => w.Locations).Include(w => w.Comments).Include(w => w.Quotes).Include(w => w.Dispatches).ThenInclude(d => d.Vendor).Where(w => w.Id == id).Select(s => new
{
s.Id,
s.InternalWONumber,
s.WorkerOrderNumber,
title = s.WorkerOrderTitle,
s.Description,
s.CreatedDate,
s.DueDate,
s.ScheduledDate,
s.CompletedDate,
s.Status,
assignee = s.AssignToUser != null ? s.AssignToUser.FirstName : "",
location = s.Locations != null ? s.Locations.Name : "",
locationId = s.LocationId,
locationZip = s.Locations != null ? (s.Locations.Zip ?? ExtractZip(s.Locations.Address1)) : "",
priority = s.Priority,
s.Problem,
s.Trade,
s.SubTrade,
s.VendorNTE,
s.Source,
s.PO,
s.TT,
poc = s.WorkOrderContacts.Select(c => new { c.Id, name = c.POC != null ? (c.POC.FirstName + " " + c.POC.LastName).Trim() : "", email = c.POC != null ? c.POC.Email : "", phone = c.POC != null ? c.POC.PhoneNumber : "" }),
category = s.WorkOrderCategories.Select(c => new { c.Id, name = c.Category != null ? c.Category.Name : "" }),
Attachments = s.workOrderAttachments.Select(a => new { a.Id, attachment = a.Attachments }),
s.BeforPhotoIssue,
s.BeforPhotoAttachment,
s.AfterPhotoIssue,
s.AfterPhotoAttachment,
s.SignOffName,
s.SignOffAttachment,
s.SignOffSignature,
Comments = s.Comments.Select(c => new { c.Id, c.CreatedDate, c.Commenttext, c.Commenter, c.RecordType, c.CommentType, c.Documents, userName = c.ApplicationUser != null ? c.ApplicationUser.FirstName : "" }),
quotes = s.Quotes.Select(q => new { code = q.QuotId, q.Id }),
auditLog = _db.WorkOrderAuditLogs
.Where(a => a.WorkOrderId == s.Id)
.OrderByDescending(a => a.CreatedAt)
.Select(a => new {
a.Id,
a.FieldName,
a.OldValue,
a.NewValue,
a.Action,
a.CreatedAt,
userName = a.User != null ? a.User.FirstName + " " + a.User.LastName : ""
}).ToList(),
dispatches = s.Dispatches.Select(d => new {
d.Id,
d.DispatchNumber,
d.PONumber,
d.NTEAmount,
d.Description,
d.Status,
d.ScheduledDate,
d.EstimatedArrivalAt,
d.EtaManualOverride,
d.CompletedDate,
d.DispatchedAt,
d.EmailSent,
vendorName = d.Vendor != null ? d.Vendor.CompanyName : "",
vendorEmail = d.Vendor != null ? d.Vendor.Email : "",
vendorId = d.VendorId
}).ToList()
}).FirstOrDefaultAsync();
if (exist == null)
{
return BadRequest(new Response { Status = "Error", Message = "ID not found!" });
}
else
{
return Ok(exist);
}
}
[HttpGet]
[Route("GetFilteredWorkorder2")]
public IActionResult GetFilteredWorkorder2([FromQuery] string[] assignto, [FromQuery] int[]? location, [FromQuery] string[]? priority, [FromQuery] string[]? status, [FromQuery] string[]? duedate, [FromQuery] string search = "", [FromQuery] string sort = "Sort Order", [FromQuery] string sortby = "Sort By", int page = 1, int pageSize = 10)
{
try
{
var totalCount = _db.workOrders.Where(w => w.istemplate != true).Count();
if (sort == "Sort Order" && sortby == "Sort By" && assignto.Count() == 0 && (duedate == null || duedate.Count() == 0) && location.Count() == 0 && (priority == null || priority.Count() == 0) && (status == null || status.Count() == 0) && search == "")
{
var data1 = _db.workOrders.Where(w => w.istemplate != true).Include(w => w.Locations).ToListAsync();
return Ok(data1);
}
//if (sort == "Sort Order" && sortby == "Sort By" && assignto.Count() == 0 && (duedate == null || duedate.Count() == 0) && location.Count() == 0 && (priority == null || priority.Count() == 0) && (status == null || status.Count() == 0) && search == "")
//{
// var data1 = _db.workOrders.Where(w => w.istemplate != true).Include(w => w.Locations).Select(s => new {s.Id, s.WorkerOrderTitle, s.DueDate, s.Status, s.Priority, s.Locations.Name }).OrderByDescending(d => d.Name).ToList().Skip((page - 1) * pageSize)
// .Take(pageSize);
// // Calculate the number of pages
// var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
// // Create a view model containing the paged items and pagination metadata
// var viewModel1 = new Pagination_DTO
// {
// Data = data1,
// PageNumber = page,
// PageSize = pageSize,
// TotalCount = totalCount,
// TotalPages = totalPages
// };
// return Ok(viewModel1);
//}
var currentDate = DateTime.UtcNow.Date;
var data = _db.workOrders.Include(w => w.Locations)
.Where(w => w.istemplate != true && (((w.WorkerOrderTitle ?? "").ToLower().Contains(search.ToLower()) || (w.WorkerOrderNumber ?? "").ToLower().Contains(search.ToLower()) || (w.Locations != null && (w.Locations.Title ?? "").ToLower().Contains(search.ToLower()))) &&
(assignto.Count() == 0 || assignto.Contains(w.AssignTo)) &&
(location.Count() == 0 || (w.LocationId.HasValue && location.Contains(w.LocationId.Value))) &&
(priority.Count() == 0 || priority.Contains(w.Priority)) &&
((status.Count() == 0 || status.Contains(w.Status))
)))
.ToList();
// Perform client-side filtering for due dates
data = data.Where(w =>
duedate.Count() == 0 ||
duedate.Any(dd => w.DueDate.HasValue && CheckDueDate(dd, w.DueDate.Value.Date, currentDate))).ToList();
// Apply sorting
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;
default:
// Handle default case or throw an exception
break;
}
}
var newdata = data.Select(s => new { s.Id, s.WorkerOrderTitle, s.DueDate, s.Status, s.Priority, Name = s.Locations != null ? s.Locations.Name : "" }).OrderByDescending(d => d.Name).ToList().Skip((page - 1) * pageSize)
.Take(pageSize);
var totalPages1 = (int)Math.Ceiling(totalCount / (double)pageSize);
// Create a view model containing the paged items and pagination metadata
var viewModel = new Pagination_DTO
{
Data = newdata,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = totalPages1
};
return Ok(viewModel);
}
catch (Exception ex)
{
var totalCount = _db.workOrders.Where(w => w.istemplate != true).Count();
var data1 = _db.workOrders.Where(w => w.istemplate != true).Include(w => w.Locations).Select(s => new
{
s.Id,
s.WorkerOrderTitle,
s.DueDate,
s.Status,
s.Priority,
Name = s.Locations != null ? s.Locations.Name : ""
}).OrderByDescending(d => d.Id).ToList().Skip((page - 1) * pageSize)
.Take(pageSize);
// Calculate the number of pages
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
// Create a view model containing the paged items and pagination metadata
var viewModel = new Pagination_DTO
{
Data = data1,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = totalPages
};
return Ok(viewModel);
}
}
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
};
}
[HttpPost]
[Route("ChangeStatus")]
public async Task<IActionResult> ChangeStatus(int id, string status)
{
try
{
var workOrder = await _workOrderDataService.GetByIdAsync(id);
if (workOrder == null)
return BadRequest(new Response { Status = "Error", Message = "Work order not found" });
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
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 Ok(new DataResponse { Message = "Updated Successfully", Status = "200", Data = WorkOrderDTO.MapToModel(workOrder) });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPost]
[Route("ChangeAssignment")]
public async Task<IActionResult> ChangeAssignment(int id, string? assignTo)
{
try
{
var workOrder = await _workOrderDataService.GetByIdAsync(id);
if (workOrder == null)
return BadRequest(new Response { Status = "Error", Message = "Work order not found" });
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
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;
var assignedName = assignedUser != null ? (assignedUser.FirstName + " " + assignedUser.LastName).Trim() : "";
return Ok(new { message = "Updated Successfully", assignedTo = assignedName });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPost]
[Route("AddComment")]
public async Task<IActionResult> AddComment([FromForm] Comments_DTO model)
{
try
{
var userId = _httpContext?.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "";
var comment = new Comments
{
UserId = userId,
WorkerOrderId = model.WorkorderId,
Commenttext = model.Text,
CommentType = model.CommentType ?? "vendor"
};
if (model.Document != null)
{
var fileUrl = await SaveFileAsync(model.Document);
comment.Documents = fileUrl;
}
var saved = await _commentDataService.AddAsync(comment);
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200", Data = saved });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPost]
[Route("AddCommentJson")]
public async Task<IActionResult> AddCommentJson([FromBody] Comments_DTO model)
{
try
{
var userId = _httpContext?.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "";
var comment = new Comments
{
UserId = userId,
WorkerOrderId = model.WorkorderId,
Commenttext = model.Text,
CommentType = model.CommentType ?? "internal",
};
var saved = await _commentDataService.AddAsync(comment);
var user = await _userDataService.GetByIdAsync(userId);
return Ok(new
{
saved.Id,
saved.CreatedDate,
saved.Commenttext,
saved.CommentType,
userName = user != null ? (user.FirstName + " " + user.LastName).Trim() : ""
});
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpGet]
[Route("GetComments")]
public async Task<IActionResult> GetComments()
{
var comments = await _commentDataService.GetAllAsync();
var data = comments.Select(s => new
{
s.Id,
s.Commenttext,
FirstName = s.ApplicationUser != null ? s.ApplicationUser.FirstName : "",
s.Documents
}).ToList();
return Ok(data);
}
[HttpGet]
[Route("GetCommentsByWorkorderId")]
public async Task<IActionResult> GetCommentsByWorkorderId(int woid)
{
var comments = await _commentDataService.GetByWorkOrderIdAsync(woid);
var data = comments.Select(s => new
{
s.Id,
s.Commenttext,
FirstName = s.ApplicationUser != null ? s.ApplicationUser.FirstName : "",
s.Documents
}).ToList();
return Ok(data);
}
[HttpGet]
[Route("GetworkordersDD")]
public async Task<IActionResult> GetworkordersDD()
{
var data = await _workOrderDataService.GetAllAsync();
return Ok(data.Where(w => w.istemplate != true));
}
[HttpGet]
[Route("Getworkorders")]
public async Task<IActionResult> Getworkorders()
{
var workOrders = await _workOrderDataService.GetAllWithDetailsAsync();
var data = 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();
return Ok(data);
}
[HttpPost]
[Route("DispatchToVendor")]
public async Task<IActionResult> DispatchToVendor([FromBody] Dispatch_DTO model)
{
try
{
var allWorkOrders = await _workOrderDataService.GetAllWithDetailsAsync();
var workOrders = allWorkOrders.Where(w => model.WorkOrderIds.Contains(w.Id)).ToList();
if (workOrders.Count == 0)
return BadRequest(new Response { Status = "Error", Message = "No work orders found" });
var primaryWO = workOrders.First();
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 userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var currentUser = userId != null ? await _userDataService.GetByIdAsync(userId) : null;
var senderName = currentUser != null ? (currentUser.FirstName + " " + currentUser.LastName).Trim() : "";
var dispatches = new List<object>();
foreach (var vendorId in model.VendorIds)
{
var vendor = await _vendorDataService.GetByIdAsync(vendorId);
if (vendor == null)
return BadRequest(new Response { Status = "Error", Message = $"Vendor {vendorId} was not found" });
if (!vendor.IsActive || (vendor.IsDeleted ?? false))
return BadRequest(new Response { Status = "Error", Message = $"Vendor {vendorId} is inactive and cannot receive a new assignment" });
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 = vendorId,
DispatchNumber = dispatchNumber,
PONumber = poNumber,
NTEAmount = model.NTEAmount,
Description = model.Description,
Status = "Sent",
ScheduledDate = model.ScheduledDate,
EstimatedArrivalAt = model.EstimatedArrivalAt,
EtaManualOverride = model.EtaManualOverride,
DispatchedAt = DateTime.UtcNow,
ReplyToAddress = replyTo,
DispatchWorkOrders = workOrders.Select(wo => new DispatchWorkOrder { WorkOrderId = wo.Id }).ToList()
};
var checklistItems = new List<DispatchChecklistItem>();
if (model.TaskListTemplateId.HasValue)
{
var template = await _taskListTemplateDataService.GetByIdWithItemsAsync(model.TaskListTemplateId.Value);
if (template?.Items != null)
{
checklistItems.AddRange(template.Items.OrderBy(i => i.SortOrder).Select((i, idx) =>
new DispatchChecklistItem { ItemText = i.ItemText, SortOrder = idx }));
}
}
if (model.CustomChecklistItems != null)
{
var startIdx = checklistItems.Count;
checklistItems.AddRange(model.CustomChecklistItems.Select((text, idx) =>
new DispatchChecklistItem { ItemText = text, SortOrder = startIdx + idx }));
}
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(model.Description ?? primaryWO.Description ?? "");
var senderNameHtml = System.Net.WebUtility.HtmlEncode(senderName);
var vendorToken = await _vendorTokens.GetOrCreateActiveTokenAsync(vendorId);
var frontendBase = _config.GetValue<string>("FrontendBaseUrl")?.TrimEnd('/') ?? "";
var portalUrl = $"{frontendBase}/v/{vendorToken.Token}/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> ${model.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:6px 8px;border:1px solid #ddd;text-align:left;'>WO #</th>
<th style='padding:6px 8px;border:1px solid #ddd;text-align:left;'>Title</th>
<th style='padding:6px 8px;border:1px solid #ddd;text-align:left;'>Priority</th>
<th style='padding:6px 8px;border:1px solid #ddd;text-align:left;'>Due Date</th>
</tr>
{woListHtml}
</table>
<h3>Description</h3>
<p>{descriptionHtml}</p>
<br/>
<div style='text-align:center;margin:20px 0;'>
<a href='{portalUrl}' style='display:inline-block;padding:12px 32px;background:#28a745;color:white;text-decoration:none;border-radius:6px;font-weight:bold;font-size:16px;'>View Work Order</a>
</div>
<br/><p style='color:#666;font-size:13px;'>{senderNameHtml}<br/>Sea Haven Industries</p>
<hr/><p style='color:#999;font-size:12px;'>Reply to this email to communicate about this dispatch.</p>";
var emailSent = await _sendMessage.SendDispatchEmail(vendor.Email, subject, html, replyTo);
dispatch.EmailSent = emailSent;
var savedDispatch = await _dispatchDataService.AddAsync(dispatch);
foreach (var wo in workOrders)
{
await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog
{
WorkOrderId = wo.Id,
UserId = userId,
FieldName = "Dispatch",
OldValue = "",
NewValue = $"{vendor.CompanyName} ({dispatchNumber})",
Action = AuditAction.Dispatch.ToString(),
CreatedAt = DateTime.UtcNow
});
if (model.EstimatedArrivalAt.HasValue)
{
await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog
{
WorkOrderId = wo.Id,
UserId = userId,
FieldName = "Vendor ETA",
OldValue = "",
NewValue = model.EstimatedArrivalAt.Value.ToUniversalTime().ToString("O"),
Action = model.EtaManualOverride ? "vendor_eta_manual_override" : "vendor_eta_set",
CreatedAt = DateTime.UtcNow
});
}
}
dispatches.Add(new { savedDispatch.Id, dispatchNumber, poNumber, vendorName = vendor.CompanyName, savedDispatch.Status, workOrderCount = workOrders.Count });
}
return Ok(new { message = "Dispatched", dispatches });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpGet("GetDispatches/{workOrderId}")]
public async Task<IActionResult> GetDispatches(int workOrderId)
{
var dispatches = await _dispatchDataService.GetByWorkOrderIdAsync(workOrderId);
var data = dispatches.OrderByDescending(d => d.DispatchedAt).Select(d => new
{
d.Id,
d.DispatchNumber,
d.PONumber,
d.NTEAmount,
d.Description,
d.Status,
d.ScheduledDate,
d.EstimatedArrivalAt,
d.EtaManualOverride,
d.CompletedDate,
d.DispatchedAt,
d.EmailSent,
vendorName = d.Vendor != null ? d.Vendor.CompanyName : "",
vendorEmail = d.Vendor != null ? d.Vendor.Email : "",
vendorId = d.VendorId
}).ToList();
return Ok(data);
}
[HttpGet("GetDispatch/{id}")]
public async Task<IActionResult> GetDispatchById(int id)
{
var d = await _db.Dispatches
.Include(x => x.Vendor)
.Include(x => x.WorkOrder)
.Where(x => x.Id == id)
.Select(x => new
{
x.Id,
x.DispatchNumber,
x.PONumber,
x.NTEAmount,
x.Description,
x.Status,
x.ScheduledDate,
x.EstimatedArrivalAt,
x.EtaManualOverride,
x.CompletedDate,
x.DispatchedAt,
x.EmailSent,
x.WorkOrderId,
x.AcknowledgedAt,
x.VerifiedBy,
x.VerifiedAt,
woNumber = x.WorkOrder != null ? x.WorkOrder.InternalWONumber : "",
woTitle = x.WorkOrder != null ? x.WorkOrder.WorkerOrderTitle : "",
vendorName = x.Vendor != null ? x.Vendor.CompanyName : "",
vendorEmail = x.Vendor != null ? x.Vendor.Email : "",
vendorId = x.VendorId,
workOrders = _db.DispatchWorkOrders
.Where(dwo => dwo.DispatchId == x.Id)
.Select(dwo => new {
dwo.WorkOrderId,
woNumber = dwo.WorkOrder != null ? dwo.WorkOrder.InternalWONumber : "",
woTitle = dwo.WorkOrder != null ? dwo.WorkOrder.WorkerOrderTitle : ""
}).ToList(),
checklistItems = _db.DispatchChecklistItems
.Where(ci => ci.DispatchId == x.Id)
.OrderBy(ci => ci.SortOrder)
.Select(ci => new {
ci.Id,
ci.ItemText,
ci.IsCompleted,
ci.CompletedBy,
ci.CompletedAt,
ci.SortOrder,
ci.WorkOrderId
}).ToList(),
signoffs = _db.DispatchSignoffs
.Where(s => s.DispatchId == x.Id)
.Select(s => new {
s.Id,
s.SignoffType,
s.Name,
s.Signature,
s.SignatureMethod,
s.SignedAt
}).ToList(),
comments = _db.Comments
.Where(c => c.DispatchId == x.Id)
.OrderBy(c => c.CreatedDate)
.Select(c => new
{
c.Id,
c.CreatedDate,
c.Commenttext,
c.Commenter,
c.CommentType,
userName = c.ApplicationUser != null ? c.ApplicationUser.FirstName + " " + c.ApplicationUser.LastName : ""
}).ToList()
}).FirstOrDefaultAsync();
if (d == null)
return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
return Ok(d);
}
[HttpPost("UpdateDispatch")]
public async Task<IActionResult> UpdateDispatch([FromBody] UpdateDispatch_DTO model)
{
try
{
var dispatch = await _db.Dispatches.FindAsync(model.Id);
if (dispatch == null)
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (dispatch.Status != model.Status)
{
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
{
WorkOrderId = dispatch.WorkOrderId ?? 0,
UserId = userId,
FieldName = $"Dispatch {dispatch.DispatchNumber} Status",
OldValue = dispatch.Status ?? "",
NewValue = model.Status ?? "",
Action = "dispatch_status_change",
CreatedAt = DateTime.UtcNow
});
}
dispatch.Status = model.Status;
dispatch.NTEAmount = model.NTEAmount;
dispatch.ScheduledDate = model.ScheduledDate;
if (dispatch.EstimatedArrivalAt != model.EstimatedArrivalAt)
{
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
{
WorkOrderId = dispatch.WorkOrderId ?? 0,
UserId = userId,
FieldName = "Vendor ETA",
OldValue = dispatch.EstimatedArrivalAt?.ToUniversalTime().ToString("O") ?? "",
NewValue = model.EstimatedArrivalAt?.ToUniversalTime().ToString("O") ?? "",
Action = model.EtaManualOverride ? "vendor_eta_manual_override" : "vendor_eta_change",
CreatedAt = DateTime.UtcNow
});
}
dispatch.EstimatedArrivalAt = model.EstimatedArrivalAt;
dispatch.EtaManualOverride = model.EtaManualOverride;
dispatch.CompletedDate = model.CompletedDate;
dispatch.Description = model.Description;
if (model.Status == "Completed" && dispatch.CompletedDate == null)
dispatch.CompletedDate = DateTime.UtcNow;
await _db.SaveChangesAsync();
return Ok(new { message = "Dispatch updated" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPost("AddDispatchComment")]
public async Task<IActionResult> AddDispatchComment([FromBody] DispatchComment_DTO model)
{
try
{
var dispatch = await _db.Dispatches.Include(d => d.Vendor).Include(d => d.WorkOrder)
.FirstOrDefaultAsync(d => d.Id == model.DispatchId);
if (dispatch == null)
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
var userId = _httpContext?.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "";
var user = !string.IsNullOrEmpty(userId) ? await _db.Users.FindAsync(userId) : null;
var senderName = user != null ? (user.FirstName + " " + user.LastName).Trim() : "";
var comment = new Comments
{
CreatedDate = DateTime.UtcNow,
UserId = userId,
WorkerOrderId = dispatch.WorkOrderId,
DispatchId = dispatch.Id,
Commenttext = model.Text,
CommentType = "dispatcher",
Commenter = !string.IsNullOrWhiteSpace(senderName) ? senderName : null,
};
_db.Comments.Add(comment);
await _db.SaveChangesAsync();
if (model.SendEmail && dispatch.Vendor?.Email != null && dispatch.WorkOrder != null)
{
var subject = $"[WO-{dispatch.WorkOrder.InternalWONumber}-{dispatch.DispatchNumber}] {dispatch.WorkOrder.WorkerOrderTitle}";
var html = $"<p>{model.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 _sendMessage.SendDispatchEmail(dispatch.Vendor.Email, subject, html, dispatch.ReplyToAddress);
}
return Ok(new
{
comment.Id,
comment.CreatedDate,
comment.Commenttext,
comment.CommentType,
userName = user != null ? (user.FirstName + " " + user.LastName).Trim() : ""
});
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
private static string? ExtractZip(string? address)
{
if (string.IsNullOrWhiteSpace(address)) return null;
var match = System.Text.RegularExpressions.Regex.Match(address, @"\b(\d{5})(?:-\d{4})?\b");
return match.Success ? match.Groups[1].Value : null;
}
[HttpPost("VerifyDispatch")]
public async Task<IActionResult> VerifyDispatch(int dispatchId)
{
try
{
var dispatch = await _db.Dispatches.FindAsync(dispatchId);
if (dispatch == null)
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
var incompleteItems = await _db.DispatchChecklistItems
.Where(c => c.DispatchId == dispatchId && !c.IsCompleted)
.CountAsync();
var signoffCount = await _db.DispatchSignoffs
.Where(s => s.DispatchId == dispatchId)
.CountAsync();
var missing = new List<string>();
if (incompleteItems > 0)
missing.Add($"{incompleteItems} checklist item(s) incomplete");
if (signoffCount < 2)
{
var existingTypes = await _db.DispatchSignoffs
.Where(s => s.DispatchId == dispatchId)
.Select(s => s.SignoffType)
.ToListAsync();
if (!existingTypes.Contains("Customer")) missing.Add("Missing Customer sign-off");
if (!existingTypes.Contains("Vendor")) missing.Add("Missing Vendor sign-off");
}
if (missing.Count > 0)
return BadRequest(new { status = "Error", message = "Cannot verify", missing });
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var user = userId != null ? await _db.Users.FindAsync(userId) : null;
var userName = user != null ? (user.FirstName + " " + user.LastName).Trim() : "";
dispatch.Status = "Verified";
dispatch.VerifiedBy = userName;
dispatch.VerifiedAt = DateTime.UtcNow;
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
{
WorkOrderId = dispatch.WorkOrderId ?? 0,
UserId = userId,
FieldName = $"Dispatch {dispatch.DispatchNumber}",
OldValue = dispatch.Status ?? "",
NewValue = "Verified",
Action = "dispatch_verified",
CreatedAt = DateTime.UtcNow
});
await _db.SaveChangesAsync();
return Ok(new { message = "Dispatch verified", verifiedBy = userName, verifiedAt = dispatch.VerifiedAt });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPost("AddDispatchSignoff")]
public async Task<IActionResult> AddDispatchSignoff([FromBody] DispatchSignoff_DTO model)
{
try
{
var dispatch = await _db.Dispatches.FindAsync(model.DispatchId);
if (dispatch == null)
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
var existing = await _db.DispatchSignoffs
.AnyAsync(s => s.DispatchId == model.DispatchId && s.SignoffType == model.SignoffType);
if (existing)
return BadRequest(new Response { Status = "Error", Message = $"{model.SignoffType} sign-off already exists" });
var signoff = new DispatchSignoff
{
DispatchId = model.DispatchId,
SignoffType = model.SignoffType,
Name = model.Name,
Signature = model.Signature,
SignatureMethod = model.SignatureMethod,
SignedAt = DateTime.UtcNow
};
_db.DispatchSignoffs.Add(signoff);
await _db.SaveChangesAsync();
return Ok(new { signoff.Id, signoff.SignoffType, signoff.Name, signoff.SignedAt, signoff.SignatureMethod });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPost("UpdateChecklistItem")]
public async Task<IActionResult> UpdateChecklistItem([FromBody] ChecklistItemUpdate_DTO model)
{
var item = await _dispatchDataService.GetChecklistItemByIdAsync(model.Id);
if (item == null)
return NotFound(new Response { Status = "Error", Message = "Checklist item not found" });
item.IsCompleted = model.IsCompleted;
item.CompletedBy = model.IsCompleted ? model.CompletedBy : null;
item.CompletedAt = model.IsCompleted ? DateTime.UtcNow : null;
await _dispatchDataService.UpdateChecklistItemAsync(item);
return Ok(new { message = "Updated", item.Id, item.IsCompleted, item.CompletedBy, item.CompletedAt });
}
[HttpPost("AddChecklistItem")]
public async Task<IActionResult> AddChecklistItem([FromBody] AddChecklistItem_DTO model)
{
var checklistItems = await _dispatchDataService.GetChecklistItemsAsync(model.DispatchId);
var maxSort = checklistItems.Any() ? checklistItems.Max(c => c.SortOrder) : -1;
var item = new DispatchChecklistItem
{
DispatchId = model.DispatchId,
WorkOrderId = model.WorkOrderId,
ItemText = model.ItemText,
SortOrder = maxSort + 1,
IsCompleted = false
};
await _dispatchDataService.AddChecklistItemAsync(item);
return Ok(new { item.Id, item.ItemText, item.SortOrder, item.IsCompleted });
}
private async Task<string> GenerateInternalWONumber()
{
return await _workOrderDataService.GenerateNextInternalWONumberAsync();
}
private async Task<string> SaveFileAsync(IFormFile file)
{
var fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.Trim('"');
var uniqueFileName = $"{Guid.NewGuid()}_{fileName}";
var uploadPath = Path.Combine("Assets", "Documents");
var fullPath = Path.Combine(_webHostEnvironment.WebRootPath, uploadPath, uniqueFileName);
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
using (var stream = System.IO.File.Create(fullPath))
{
await file.CopyToAsync(stream);
}
var request = _httpContext.HttpContext!.Request;
var domain = $"{request.Scheme}://{request.Host}";
return $"{domain}/{uploadPath.Replace("\\", "/")}/{uniqueFileName}";
}
public class WorkorderFilterVM
{
public int? Id { get; set; }
public int? LocationId { get; set; }
public string? WorkerOrderNumber { get; set; }
public string? BillingAddress { get; set; } = "Amazon.com Services LLC 410 Terry Ave,North Seattle, Washington, 98109";
public string? WorkerOrderTitle { get; set; }
public string? LocationName { get; set; }
public string? AssignToUserName { get; set; }
public string? Priority { get; set; }
public string? Status { get; set; }
public DateTime? DueDate { get; set; }
public string FormattedDueDate
{
get { return DueDate.HasValue ? DueDate.Value.ToString("MM/dd/yyyy") : ""; }
}
public string? Dispatcher { get; set; }
public string? Scheduler { get; set; }
public string? PO { get; set; }
public string? TT { get; set; }
}
}
}