using Api.SeaHavenIndustries.DTOs; using Data.SeaHavenIndustries; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; 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; namespace Api.SeaHavenIndustries.Controllers { // [Authorize] [ApiController] [Route("api/WorkOrder")] [Route("api/workorders")] public class WorkOrderController : Controller { private readonly UserManager _userManager; private readonly ApplicationDbContext _db; private readonly IWebHostEnvironment _webHostEnvironment; IHttpContextAccessor _httpContext; private readonly Helper.SendMessage _sendMessage; public WorkOrderController(UserManager userManager, ApplicationDbContext db, IWebHostEnvironment webHostEnvironment, IHttpContextAccessor httpContext, Helper.SendMessage sendMessage) { _userManager = userManager; _db = db; _webHostEnvironment = webHostEnvironment; _httpContext = httpContext; _sendMessage = sendMessage; } [HttpPost] [Route("AddWorkorder")] public async Task Addworkorder([FromBody] Workorder_DTO model) { try { string userId = User.FindFirstValue(ClaimTypes.NameIdentifier); WorkOrder workOrder = new WorkOrder(); workOrder.InternalWONumber = await GenerateInternalWONumber(); workOrder.WorkerOrderNumber = model.WorkerOrderNumber; workOrder.WorkerOrderTitle = model.WorkerOrderTitle; workOrder.Description = model.Description; workOrder.TT = model.TT; workOrder.PO = model.PO; workOrder.AssignTo = model.AssignTo; workOrder.DueDate = model.DueDate; workOrder.Priority = model.Priority; workOrder.istemplate = model.istemplate; //workOrder.CategoryId = model.CategoryId; workOrder.LocationId = model.LocationId; workOrder.Status = "Open"; workOrder.istemplate = model.istemplate; workOrder.BeforPhotoIssue = model.BeforPhotoIssue; workOrder.AfterPhotoIssue = model.AfterPhotoIssue; workOrder.SignOffName = model.SignOffName; workOrder.SignOffSignature = model.SignOffSignature; workOrder.WorkOrderContacts = workOrder.WorkOrderContacts == null ? new List() : workOrder.WorkOrderContacts; List contactIds = model.GetContactIds(); workOrder.WorkOrderCategories = workOrder.WorkOrderCategories == null ? new List() : workOrder.WorkOrderCategories; List categoryIds = model.GetCategoryIds(); // Create new records for the associations (pseudo-code) foreach (int contactId in contactIds) { // Create association with contact workOrder.WorkOrderContacts.Add(new WorkOrderContacts { ContactId = contactId }); } foreach (int categoryId in categoryIds) { // Create association with category workOrder.WorkOrderCategories.Add(new WorkOrderCategories { CategoryId = categoryId }); } #region Images of workorder workOrder.workOrderAttachments = workOrder.workOrderAttachments == null ? new List() : workOrder.workOrderAttachments; if (model.Attachments != null) { foreach (var attachment in model.Attachments) { string fileName = ContentDispositionHeaderValue.Parse(attachment.ContentDisposition).FileName.Trim('"'); string uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; string uploadProfilePath = Path.Combine("Assets", "Documents"); string fullPath = Path.Combine(_webHostEnvironment.WebRootPath, uploadProfilePath, uniqueFileName); Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); // Copy the file to the server using (var ms = System.IO.File.Create(fullPath)) { await attachment.CopyToAsync(ms); } // Update the user's image URL var request = _httpContext.HttpContext.Request; var domain = $"{request.Scheme}://{request.Host}"; var imagesPath = domain + "/" + Path.Combine(uploadProfilePath, uniqueFileName); workOrder.workOrderAttachments.Add(new WorkOrderAttachments { Attachments = imagesPath }); } } if (model.BeforPhotoAttachment != null) { string fileName = ContentDispositionHeaderValue.Parse(model.BeforPhotoAttachment.ContentDisposition).FileName.Trim('"'); string uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; string uploadProfilePath = Path.Combine("Assets", "Documents"); string fullPath = Path.Combine(_webHostEnvironment.WebRootPath, uploadProfilePath, uniqueFileName); Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); // Copy the file to the server using (var ms = System.IO.File.Create(fullPath)) { await model.BeforPhotoAttachment.CopyToAsync(ms); } // Update the user's image URL var request = _httpContext.HttpContext.Request; var domain = $"{request.Scheme}://{request.Host}"; workOrder.BeforPhotoAttachment = domain + "/" + Path.Combine(uploadProfilePath, uniqueFileName); } if (model.AfterPhotoAttachment != null) { string fileName = ContentDispositionHeaderValue.Parse(model.AfterPhotoAttachment.ContentDisposition).FileName.Trim('"'); string uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; string uploadProfilePath = Path.Combine("Assets", "Documents"); string fullPath = Path.Combine(_webHostEnvironment.WebRootPath, uploadProfilePath, uniqueFileName); Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); // Copy the file to the server using (var ms = System.IO.File.Create(fullPath)) { await model.AfterPhotoAttachment.CopyToAsync(ms); } // Update the user's image URL var request = _httpContext.HttpContext.Request; var domain = $"{request.Scheme}://{request.Host}"; workOrder.AfterPhotoAttachment = domain + "/" + Path.Combine(uploadProfilePath, uniqueFileName); } if (model.SignOffAttachment != null) { string fileName = ContentDispositionHeaderValue.Parse(model.SignOffAttachment.ContentDisposition).FileName.Trim('"'); string uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; string uploadProfilePath = Path.Combine("Assets", "Documents"); string fullPath = Path.Combine(_webHostEnvironment.WebRootPath, uploadProfilePath, uniqueFileName); Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); // Copy the file to the server using (var ms = System.IO.File.Create(fullPath)) { await model.SignOffAttachment.CopyToAsync(ms); } // Update the user's image URL var request = _httpContext.HttpContext.Request; var domain = $"{request.Scheme}://{request.Host}"; workOrder.SignOffAttachment = domain + "/" + Path.Combine(uploadProfilePath, uniqueFileName); } #endregion _db.Add(workOrder); _db.SaveChanges(); return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" }); } catch (Exception ex) { return BadRequest(new Response { Status = "Error", Message = ex.Message }); } } [HttpPost] [HttpPut("{id:int}")] [Route("EditWorkorder")] public async Task Editworkorder([FromBody] EditWorkorder_DTO model) { try { var workOrder = _db.workOrders.Where(w => w.Id == model.Id).FirstOrDefault(); if (workOrder == null) return BadRequest(new Response { Status = "Error", Message = "Work order not found" }); var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); var now = DateTime.UtcNow; void LogChange(string field, string? oldVal, string? newVal) { if (oldVal == newVal) return; _db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog { WorkOrderId = workOrder.Id, UserId = userId, FieldName = field, OldValue = oldVal ?? "", NewValue = newVal ?? "", Action = "edit", CreatedAt = now }); } LogChange("WorkOrderNumber", workOrder.WorkerOrderNumber, model.WorkerOrderNumber); LogChange("Title", workOrder.WorkerOrderTitle, model.WorkerOrderTitle); LogChange("Description", workOrder.Description, model.Description); LogChange("TT", workOrder.TT, model.TT); LogChange("PO", workOrder.PO, model.PO); LogChange("AssignTo", workOrder.AssignTo, model.AssignTo); LogChange("DueDate", workOrder.DueDate?.ToString("yyyy-MM-dd"), model.DueDate?.ToString("yyyy-MM-dd")); LogChange("Priority", workOrder.Priority, model.Priority); LogChange("LocationId", workOrder.LocationId?.ToString(), model.LocationId?.ToString()); LogChange("Status", workOrder.Status, model.Status); LogChange("Problem", workOrder.Problem, model.Problem); LogChange("Trade", workOrder.Trade, model.Trade); LogChange("SubTrade", workOrder.SubTrade, model.SubTrade); LogChange("VendorNTE", workOrder.VendorNTE?.ToString("F2"), model.VendorNTE?.ToString("F2")); LogChange("ScheduledDate", workOrder.ScheduledDate?.ToString("yyyy-MM-dd"), model.ScheduledDate?.ToString("yyyy-MM-dd")); LogChange("CompletedDate", workOrder.CompletedDate?.ToString("yyyy-MM-dd"), model.CompletedDate?.ToString("yyyy-MM-dd")); LogChange("Source", workOrder.Source, model.Source); workOrder.WorkerOrderNumber = model.WorkerOrderNumber; workOrder.WorkerOrderTitle = model.WorkerOrderTitle; workOrder.Description = model.Description; workOrder.TT = model.TT; workOrder.PO = model.PO; workOrder.AssignTo = model.AssignTo; workOrder.DueDate = model.DueDate; workOrder.Priority = model.Priority; workOrder.LocationId = model.LocationId; workOrder.Status = model.Status; workOrder.Problem = model.Problem; workOrder.Trade = model.Trade; workOrder.SubTrade = model.SubTrade; workOrder.VendorNTE = model.VendorNTE; workOrder.ScheduledDate = model.ScheduledDate; workOrder.CompletedDate = model.CompletedDate; workOrder.Source = model.Source; workOrder.BeforPhotoIssue = model.BeforPhotoIssue; workOrder.AfterPhotoIssue = model.AfterPhotoIssue; workOrder.SignOffName = model.SignOffName; workOrder.SignOffSignature = model.SignOffSignature; var existcontact = _db.WorkOrderContacts.Where(w => w.WorkorderId == model.Id).ToList(); if (existcontact.Count() > 0) { _db.WorkOrderContacts.RemoveRange(existcontact); } var existcategory = _db.workOrderCategories.Where(w => w.WorkorderId == model.Id).ToList(); if (existcategory.Count() > 0) { _db.workOrderCategories.RemoveRange(existcategory); } workOrder.WorkOrderContacts = workOrder.WorkOrderContacts == null ? new List() : workOrder.WorkOrderContacts; List contactIds = model.GetContactIds(); workOrder.WorkOrderCategories = workOrder.WorkOrderCategories == null ? new List() : workOrder.WorkOrderCategories; List categoryIds = model.GetCategoryIds(); // Create new records for the associations (pseudo-code) foreach (int contactId in contactIds) { // Create association with contact workOrder.WorkOrderContacts.Add(new WorkOrderContacts { ContactId = contactId }); } foreach (int categoryId in categoryIds) { // Create association with category workOrder.WorkOrderCategories.Add(new WorkOrderCategories { CategoryId = categoryId }); } #region Images of workorder workOrder.workOrderAttachments = workOrder.workOrderAttachments == null ? new List() : workOrder.workOrderAttachments; if (model.Attachments != null) { foreach (var attachment in model.Attachments) { string fileName = ContentDispositionHeaderValue.Parse(attachment.ContentDisposition).FileName.Trim('"'); string uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; string uploadProfilePath = Path.Combine("Assets", "Documents"); string fullPath = Path.Combine(_webHostEnvironment.WebRootPath, uploadProfilePath, uniqueFileName); Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); using (var ms = System.IO.File.Create(fullPath)) { await attachment.CopyToAsync(ms); } var request = _httpContext.HttpContext.Request; var domain = $"{request.Scheme}://{request.Host}"; var imagesPath = domain + "/" + Path.Combine(uploadProfilePath, uniqueFileName); workOrder.workOrderAttachments.Add(new WorkOrderAttachments { Attachments = imagesPath }); } } if (model.BeforPhotoAttachment != null) { string fileName = ContentDispositionHeaderValue.Parse(model.BeforPhotoAttachment.ContentDisposition).FileName.Trim('"'); string uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; string uploadProfilePath = Path.Combine("Assets", "Documents"); string fullPath = Path.Combine(_webHostEnvironment.WebRootPath, uploadProfilePath, uniqueFileName); Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); // Copy the file to the server using (var ms = System.IO.File.Create(fullPath)) { await model.BeforPhotoAttachment.CopyToAsync(ms); } // Update the user's image URL var request = _httpContext.HttpContext.Request; var domain = $"{request.Scheme}://{request.Host}"; workOrder.BeforPhotoAttachment = domain + "/" + Path.Combine(uploadProfilePath, uniqueFileName); } if (model.AfterPhotoAttachment != null) { string fileName = ContentDispositionHeaderValue.Parse(model.AfterPhotoAttachment.ContentDisposition).FileName.Trim('"'); string uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; string uploadProfilePath = Path.Combine("Assets", "Documents"); string fullPath = Path.Combine(_webHostEnvironment.WebRootPath, uploadProfilePath, uniqueFileName); Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); // Copy the file to the server using (var ms = System.IO.File.Create(fullPath)) { await model.AfterPhotoAttachment.CopyToAsync(ms); } // Update the user's image URL var request = _httpContext.HttpContext.Request; var domain = $"{request.Scheme}://{request.Host}"; workOrder.AfterPhotoAttachment = domain + "/" + Path.Combine(uploadProfilePath, uniqueFileName); } if (model.SignOffAttachment != null) { string fileName = ContentDispositionHeaderValue.Parse(model.SignOffAttachment.ContentDisposition).FileName.Trim('"'); string uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; string uploadProfilePath = Path.Combine("Assets", "Documents"); string fullPath = Path.Combine(_webHostEnvironment.WebRootPath, uploadProfilePath, uniqueFileName); Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); // Copy the file to the server using (var ms = System.IO.File.Create(fullPath)) { await model.SignOffAttachment.CopyToAsync(ms); } // Update the user's image URL var request = _httpContext.HttpContext.Request; var domain = $"{request.Scheme}://{request.Host}"; workOrder.SignOffAttachment = domain + "/" + Path.Combine(uploadProfilePath, uniqueFileName); } #endregion _db.SaveChanges(); return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" }); } catch (Exception ex) { return BadRequest(new Response { Status = "Error", Message = ex.Message }); } } // ----------------------------------- // GET: api/WorkOrder/GetWorkOrderList // ----------------------------------- [HttpGet("GetWorkOrderList")] public IActionResult GetWorkOrderList(string? search = "", int page = 1, int pageSize = 12, string? sortBy = "lastUpdated", string? sortDir = "desc", string? status = "", string? assignee = "", int? locationId = null) { search ??= ""; var query = _db.workOrders .Include(w => w.Locations) .Include(w => w.AssignToUser) .Where(w => w.istemplate != true); if (locationId.HasValue) query = query.Where(w => w.LocationId == locationId.Value); if (!string.IsNullOrWhiteSpace(status)) query = query.Where(w => w.Status == status); if (assignee == "__unassigned") query = query.Where(w => w.AssignTo == null || w.AssignTo == ""); else if (!string.IsNullOrWhiteSpace(assignee)) query = query.Where(w => w.AssignToUser != null && (w.AssignToUser.FirstName + " " + w.AssignToUser.LastName).Contains(assignee)); if (!string.IsNullOrWhiteSpace(search)) { var s = search.ToLower(); query = query.Where(w => (w.InternalWONumber ?? "").ToLower().Contains(s) || (w.WorkerOrderNumber ?? "").ToLower().Contains(s) || (w.WorkerOrderTitle ?? "").ToLower().Contains(s) || (w.Locations.Title ?? "").ToLower().Contains(s) || (w.Locations.Name ?? "").ToLower().Contains(s) ); } var totalCount = query.Count(); var isAsc = sortDir?.ToLower() == "asc"; IOrderedQueryable ordered = (sortBy?.ToLower()) switch { "number" => isAsc ? query.OrderBy(w => w.InternalWONumber) : query.OrderByDescending(w => w.InternalWONumber), "title" => isAsc ? query.OrderBy(w => w.WorkerOrderTitle) : query.OrderByDescending(w => w.WorkerOrderTitle), "location" => isAsc ? query.OrderBy(w => w.Locations != null ? w.Locations.Name : "") : query.OrderByDescending(w => w.Locations != null ? w.Locations.Name : ""), "priority" => isAsc ? query.OrderBy(w => w.Priority) : query.OrderByDescending(w => w.Priority), "status" => isAsc ? query.OrderBy(w => w.Status) : query.OrderByDescending(w => w.Status), "createddate" => isAsc ? query.OrderBy(w => w.CreatedDate) : query.OrderByDescending(w => w.CreatedDate), "assignedto" => isAsc ? query.OrderBy(w => w.AssignToUser != null ? w.AssignToUser.FirstName : "") : query.OrderByDescending(w => w.AssignToUser != null ? w.AssignToUser.FirstName : ""), "lastupdated" => isAsc ? query.OrderBy(w => w.LastModificationTime) : query.OrderByDescending(w => w.LastModificationTime), _ => query.OrderByDescending(w => w.Id) }; var data = ordered .Skip((page - 1) * pageSize) .Take(pageSize) .Select(w => new { id = w.Id, internalWONumber = w.InternalWONumber, workOrderNumber = w.WorkerOrderNumber, number = w.InternalWONumber ?? w.WorkerOrderNumber, title = w.WorkerOrderTitle, location = w.Locations != null ? w.Locations.Name : "", locationName = w.Locations != null ? w.Locations.Name : "", priority = w.Priority, status = w.Status, createdDate = w.CreatedDate, dueDate = w.DueDate, assignedTo = w.AssignToUser != null ? w.AssignToUser.FirstName + " " + w.AssignToUser.LastName : "", employeeName = w.AssignToUser != null ? w.AssignToUser.FirstName + " " + w.AssignToUser.LastName : "", lastUpdated = new[] { w.LastModificationTime, w.Comments.Max(c => (DateTime?)c.CreatedDate), _db.WorkOrderAuditLogs.Where(a => a.WorkOrderId == w.Id).Max(a => (DateTime?)a.CreatedAt) }.Where(d => d.HasValue).Max() }) .ToList(); 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 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 exist = _db.workOrders.Where(w => w.Id == id).FirstOrDefault(); if (exist != null) { var quotes = _db.Quotes.Where(w => w.WorkorderId == id).ToList(); if (quotes.Count() > 0) { foreach (var itm in quotes) { var existquotes = _db.Quotes.Where(w => w.Id == itm.Id).FirstOrDefault(); existquotes.WorkorderId = null; } } var attachments = _db.workOrderAttachments.Where(w => w.WorkorderId == id).ToList(); if (attachments.Count() > 0) { _db.RemoveRange(attachments); } var contacts = _db.WorkOrderContacts.Where(w => w.WorkorderId == id).ToList(); if (contacts.Count() > 0) { _db.RemoveRange(contacts); } var category = _db.workOrderCategories.Where(w => w.WorkorderId == id).ToList(); if (category.Count() > 0) { _db.RemoveRange(category); } var comments = _db.Comments.Where(w => w.WorkerOrderId == id).ToList(); if (comments.Count() > 0) { _db.RemoveRange(comments); } _db.Remove(exist); } else { return BadRequest("ID not matched!"); } _db.SaveChanges(); 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.ZipCode ?? ExtractZip(s.Locations.Address)) : "", 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 data = await _db.workOrders.Where(w => w.Id == id).FirstOrDefaultAsync(); if (data == null) return BadRequest(new Response { Status = "Error", Message = "Work order not found" }); if (data.Status != status) { _db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog { WorkOrderId = id, UserId = User.FindFirstValue(ClaimTypes.NameIdentifier), FieldName = "Status", OldValue = data.Status ?? "", NewValue = status, Action = "status_change", CreatedAt = DateTime.UtcNow }); } data.Status = status; await _db.SaveChangesAsync(); return Ok(new DataResponse { Message = "Updated Successfully", Status = "200", Data = data }); } 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 data = await _db.workOrders.Where(w => w.Id == id).FirstOrDefaultAsync(); if (data == null) return BadRequest(new Response { Status = "Error", Message = "Work order not found" }); if (data.AssignTo != assignTo) { var oldUser = data.AssignTo != null ? await _db.Users.FindAsync(data.AssignTo) : null; var newUser = assignTo != null ? await _db.Users.FindAsync(assignTo) : null; _db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog { WorkOrderId = id, UserId = User.FindFirstValue(ClaimTypes.NameIdentifier), FieldName = "AssignedTo", OldValue = oldUser != null ? (oldUser.FirstName + " " + oldUser.LastName).Trim() : "", NewValue = newUser != null ? (newUser.FirstName + " " + newUser.LastName).Trim() : "Unassigned", Action = "assignment_change", CreatedAt = DateTime.UtcNow }); } data.AssignTo = assignTo; await _db.SaveChangesAsync(); var assignedUser = assignTo != null ? await _db.Users.FindAsync(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 ?? ""; Comments comments = new Comments(); comments.CreatedDate = DateTime.Now; comments.UserId = userId; comments.WorkerOrderId = model.WorkorderId; comments.Commenttext = model.Text; comments.CommentType = model.CommentType ?? "vendor"; if (model.Document != null) { string fileName = ContentDispositionHeaderValue.Parse(model.Document.ContentDisposition).FileName.Trim('"'); string uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; string uploadProfilePath = Path.Combine("Assets", "Documents"); string fullPath = Path.Combine(_webHostEnvironment.WebRootPath, uploadProfilePath, uniqueFileName); Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); // Copy the file to the server using (var ms = System.IO.File.Create(fullPath)) { await model.Document.CopyToAsync(ms); } // Update the user's image URL var request = _httpContext.HttpContext.Request; var domain = $"{request.Scheme}://{request.Host}"; comments.Documents = domain + "/" + Path.Combine(uploadProfilePath, uniqueFileName); } _db.Comments.Add(comments); await _db.SaveChangesAsync(); return Ok(new DataResponse { Message = "Updated Successfully", Status = "200", Data = comments }); } 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 { CreatedDate = DateTime.UtcNow, UserId = userId, WorkerOrderId = model.WorkorderId, Commenttext = model.Text, CommentType = model.CommentType ?? "internal", }; _db.Comments.Add(comment); await _db.SaveChangesAsync(); var user = await _db.Users.FindAsync(userId); 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 }); } } [HttpGet] [Route("GetComments")] public async Task GetComments() { var data = await _db.Comments.Include(i => i.ApplicationUser).Select(s => new { s.Id, s.Commenttext, FirstName = s.ApplicationUser != null ? s.ApplicationUser.FirstName : "", s.Documents }).ToListAsync(); return Ok(data); } [HttpGet] [Route("GetCommentsByWorkorderId")] public async Task GetCommentsByWorkorderId(int woid) { var data = await _db.Comments.Where(w => w.WorkerOrderId == woid).Include(i => i.ApplicationUser).Select(s => new { s.Id, s.Commenttext, FirstName = s.ApplicationUser != null ? s.ApplicationUser.FirstName : "", s.Documents }).ToListAsync(); return Ok(data); } [HttpGet] [Route("GetworkordersDD")] public async Task GetworkordersDD() { _db.ChangeTracker.Clear(); var data = await _db.workOrders.Where(w => w.istemplate != true).Include(w => w.Locations).Include(w => w.AssignToUser).ToListAsync(); return Ok(data); } [HttpGet] [Route("Getworkorders")] public async Task Getworkorders() { _db.ChangeTracker.Clear(); var data = await _db.workOrders .Where(w => w.istemplate != true) .AsSplitQuery() .Include(w => w.Locations) .Include(w => w.AssignToUser).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, }).ToListAsync(); return Ok(data); } [HttpPost] [Route("DispatchToVendor")] public async Task DispatchToVendor([FromBody] Dispatch_DTO model) { try { var workOrders = await _db.workOrders.Include(w => w.Locations) .Where(w => model.WorkOrderIds.Contains(w.Id)) .ToListAsync(); if (workOrders.Count == 0) return BadRequest(new Response { Status = "Error", Message = "No work orders found" }); var primaryWO = workOrders.First(); var lastPO = await _db.Dispatches .Where(d => d.PONumber != null) .OrderByDescending(d => d.PONumber) .Select(d => d.PONumber) .FirstOrDefaultAsync(); int nextPO = 1; if (lastPO != null && lastPO.StartsWith("VPO-") && int.TryParse(lastPO.Substring(4), out var lastNum)) nextPO = lastNum + 1; var lastDSP = await _db.Dispatches .Where(d => d.DispatchNumber != null) .OrderByDescending(d => d.DispatchNumber) .Select(d => d.DispatchNumber) .FirstOrDefaultAsync(); int nextDSP = 1; if (lastDSP != null && lastDSP.StartsWith("DSP-") && int.TryParse(lastDSP.Substring(4), out var lastDspNum)) nextDSP = lastDspNum + 1; var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); var currentUser = userId != null ? await _db.Users.FindAsync(userId) : null; var senderName = currentUser != null ? (currentUser.FirstName + " " + currentUser.LastName).Trim() : ""; var dispatches = new List(); foreach (var vendorId in model.VendorIds) { var vendor = await _db.Vendors.FindAsync(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, AcceptToken = Guid.NewGuid().ToString(), DispatchWorkOrders = workOrders.Select(wo => new DispatchWorkOrder { WorkOrderId = wo.Id }).ToList() }; var checklistItems = new List(); if (model.TaskListTemplateId.HasValue) { var template = await _db.TaskListTemplates .Include(t => t.Items) .FirstOrDefaultAsync(t => t.Id == 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 => $"{wo.InternalWONumber}" + $"{wo.WorkerOrderTitle}" + $"{wo.Priority}" + $"{wo.DueDate?.ToString("yyyy-MM-dd") ?? "N/A"}")); var locationName = primaryWO.Locations?.Name ?? ""; var locationAddr = primaryWO.Locations?.Address ?? ""; var subject = $"[{dispatchNumber}] {(workOrders.Count > 1 ? $"{workOrders.Count} Work Orders" : primaryWO.WorkerOrderTitle)} at {locationName}"; 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

{model.Description ?? primaryWO.Description}



{senderName}
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; _db.Dispatches.Add(dispatch); foreach (var wo in workOrders) { _db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog { WorkOrderId = wo.Id, UserId = userId, FieldName = "Dispatch", OldValue = "", NewValue = $"{vendor.CompanyName} ({dispatchNumber})", Action = "dispatch", CreatedAt = DateTime.UtcNow }); } dispatches.Add(new { dispatch.Id, dispatchNumber, poNumber, vendorName = vendor.CompanyName, dispatch.Status, workOrderCount = workOrders.Count }); } await _db.SaveChangesAsync(); 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 dispatchIds = await _db.DispatchWorkOrders .Where(dwo => dwo.WorkOrderId == workOrderId) .Select(dwo => dwo.DispatchId) .Distinct() .ToListAsync(); var directIds = await _db.Dispatches .Where(d => d.WorkOrderId == workOrderId) .Select(d => d.Id) .ToListAsync(); var allIds = dispatchIds.Union(directIds).Distinct().ToList(); var data = await _db.Dispatches .Where(d => allIds.Contains(d.Id)) .Include(d => d.Vendor) .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 }) .ToListAsync(); 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 comment = new Comments { CreatedDate = DateTime.UtcNow, UserId = userId, WorkerOrderId = dispatch.WorkOrderId, DispatchId = dispatch.Id, Commenttext = model.Text, CommentType = "vendor", }; _db.Comments.Add(comment); await _db.SaveChangesAsync(); var user = await _db.Users.FindAsync(userId); var senderName = user != null ? (user.FirstName + " " + user.LastName).Trim() : ""; 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 _db.DispatchChecklistItems.FindAsync(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 _db.SaveChangesAsync(); return Ok(new { message = "Updated", item.Id, item.IsCompleted, item.CompletedBy, item.CompletedAt }); } [HttpPost("AddChecklistItem")] public async Task AddChecklistItem([FromBody] AddChecklistItem_DTO model) { var maxSort = await _db.DispatchChecklistItems .Where(c => c.DispatchId == model.DispatchId) .MaxAsync(c => (int?)c.SortOrder) ?? -1; var item = new DispatchChecklistItem { DispatchId = model.DispatchId, WorkOrderId = model.WorkOrderId, ItemText = model.ItemText, SortOrder = maxSort + 1, IsCompleted = false }; _db.DispatchChecklistItems.Add(item); await _db.SaveChangesAsync(); return Ok(new { item.Id, item.ItemText, item.SortOrder, item.IsCompleted }); } private async Task GenerateInternalWONumber() { var lastWO = await _db.workOrders .Where(w => w.InternalWONumber != null) .OrderByDescending(w => w.InternalWONumber) .Select(w => w.InternalWONumber) .FirstOrDefaultAsync(); int next = 10000001; if (lastWO != null && int.TryParse(lastWO, out var lastNum)) next = lastNum + 1; return next.ToString("D8"); } 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; } } } }