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.Helpers; using SeaHaven.DataServices.Interfaces; namespace Api.SeaHavenIndustries.Controllers { [Authorize] [ApiController] [Route("api/WorkOrder")] [Route("api/workorders")] public class WorkOrderController : Controller { private readonly UserManager _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; public WorkOrderController( UserManager 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) { _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; } [HttpGet("board")] public async Task GetBoard( [FromQuery, BindRequired] DateOnly weekStart, [FromQuery] DateOnly? weekEnd = null, [FromQuery] List? dispatchers = null, [FromQuery] bool myWorkOrders = false, [FromQuery] List? types = null, [FromQuery] string? search = null) { var resolvedWeekEnd = weekEnd ?? weekStart.AddDays(4); var weekValidationError = WorkOrderOperationalWeek.ValidateWeekWindow(weekStart, resolvedWeekEnd); if (weekValidationError != null) return BadRequest(weekValidationError); 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); } [HttpGet("lookups/dispatchers")] public async Task GetDispatcherLookups() { var result = await _workOrderBoardService.GetDispatcherLookupsAsync(); return Ok(result); } [HttpPost] [Route("AddWorkorder")] public async Task 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 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 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); } [HttpDelete] [Route("DeleteWorkorder")] public async Task 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 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 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.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 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 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 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 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 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 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 GetworkordersDD() { var data = await _workOrderDataService.GetAllAsync(); return Ok(data.Where(w => w.istemplate != true)); } [HttpGet] [Route("Getworkorders")] public async Task 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 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(); foreach (var vendorId in model.VendorIds) { var vendor = await _vendorDataService.GetByIdAsync(vendorId); if (vendor == null) continue; 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, DispatchedAt = DateTime.UtcNow, ReplyToAddress = replyTo, DispatchWorkOrders = workOrders.Select(wo => new DispatchWorkOrder { WorkOrderId = wo.Id }).ToList() }; var checklistItems = new List(); 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 => $"{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(model.Description ?? primaryWO.Description ?? ""); var senderNameHtml = System.Net.WebUtility.HtmlEncode(senderName); var vendorToken = await _vendorTokens.GetOrCreateActiveTokenAsync(vendorId); var frontendBase = _config.GetValue("FrontendBaseUrl")?.TrimEnd('/') ?? ""; var portalUrl = $"{frontendBase}/v/{vendorToken.Token}/dashboard"; var html = $@"

Work Order Dispatch — {dispatchNumber}

Location: {locationName} {locationAddr}

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

Work Orders

{woListHtml}
WO # Title Priority Due Date

Description

{descriptionHtml}



{senderNameHtml}
Sea Haven Industries


Reply to this email to communicate about this dispatch.

"; 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 }); } 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 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.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 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.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 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; 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 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 = $"

{model.Text}


{senderName}
Sea Haven Industries


Reply to this email to respond.

"; 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 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(); 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 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 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 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 GenerateInternalWONumber() { return await _workOrderDataService.GenerateNextInternalWONumberAsync(); } private async Task 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; } } } }