shoc-backend/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs
2026-04-28 18:55:14 -05:00

1571 lines
76 KiB
C#

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<ApplicationUser> _userManager;
private readonly ApplicationDbContext _db;
private readonly IWebHostEnvironment _webHostEnvironment;
IHttpContextAccessor _httpContext;
private readonly Helper.SendMessage _sendMessage;
private readonly Helper.VendorPortalTokenService _vendorTokens;
private readonly IConfiguration _config;
public WorkOrderController(UserManager<ApplicationUser> userManager, ApplicationDbContext db, IWebHostEnvironment webHostEnvironment, IHttpContextAccessor httpContext, Helper.SendMessage sendMessage, Helper.VendorPortalTokenService vendorTokens, IConfiguration config)
{
_userManager = userManager;
_db = db;
_webHostEnvironment = webHostEnvironment;
_httpContext = httpContext;
_sendMessage = sendMessage;
_vendorTokens = vendorTokens;
_config = config;
}
[HttpPost]
[Route("AddWorkorder")]
public async Task<IActionResult> 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<WorkOrderContacts>() : workOrder.WorkOrderContacts;
List<int> contactIds = model.GetContactIds();
workOrder.WorkOrderCategories = workOrder.WorkOrderCategories == null ? new List<WorkOrderCategories>() : workOrder.WorkOrderCategories;
List<int> 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<WorkOrderAttachments>() : 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<IActionResult> 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<WorkOrderContacts>() : workOrder.WorkOrderContacts;
List<int> contactIds = model.GetContactIds();
workOrder.WorkOrderCategories = workOrder.WorkOrderCategories == null ? new List<WorkOrderCategories>() : workOrder.WorkOrderCategories;
List<int> 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<WorkOrderAttachments>() : 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<WorkOrder> 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<WorkOrder> query = _db.workOrders.Where(w => w.istemplate != true).Include(w => w.Locations);
if (!string.IsNullOrEmpty(search))
{
var s = search.ToLower();
query = query.Where(w =>
(w.WorkerOrderTitle ?? "").ToLower().Contains(s) ||
(w.WorkerOrderNumber ?? "").ToLower().Contains(s) ||
(w.Locations != null && (w.Locations.Title ?? "").ToLower().Contains(s)));
}
if (assignto != null && assignto.Length > 0)
{
if (assignto[0] != "" && assignto[0] != null)
query = query.Where(w => assignto.Contains(w.AssignTo));
}
if (location != null && location.Length > 0)
{
if (location[0] != 0)
query = query.Where(w => location.Contains(w.LocationId ?? 0));
}
if (priority != null && priority.Length > 0)
{
if (priority[0] != "" && priority[0] != null)
query = query.Where(w => priority.Contains(w.Priority));
}
if (status != null && status.Length > 0)
{
if (status[0] != "" && status[0] != null)
query = query.Where(w => status.Contains(w.Status));
}
var data = query.ToList();
// Perform client-side filtering for due dates
data = data.Where(w =>
duedate == null || duedate.Length == 0 || duedate[0] == null ||
duedate.Any(dd => w.DueDate.HasValue && CheckDueDate(dd, w.DueDate.Value.Date, DateTime.UtcNow.Date))).ToList();
// Apply sorting
if (!string.IsNullOrEmpty(sort) && !string.IsNullOrEmpty(sortby) && sortby != "")
{
switch (sortby)
{
case "Name":
data = (sort.ToLower() == "ascending") ? data.OrderBy(w => w.WorkerOrderTitle).ToList() : data.OrderByDescending(w => w.WorkerOrderTitle).ToList();
break;
case "Due Date":
data = (sort.ToLower() == "ascending") ? data.OrderBy(w => w.DueDate).ToList() : data.OrderByDescending(w => w.DueDate).ToList();
break;
case "Severity":
data = (sort.ToLower() == "ascending") ? data.OrderBy(w => w.Priority).ToList() : data.OrderByDescending(w => w.Priority).ToList();
break;
default:
// Handle default case or throw an exception
break;
}
}
var totalCount = data.Count();
var pagedData = data
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(s => new { s.Id, s.WorkerOrderTitle, s.DueDate, s.Status, s.Priority, Name = s.Locations != null ? s.Locations.Name : "" })
.OrderByDescending(d => d.Name)
.ToList();
var totalPages1 = (int)Math.Ceiling(totalCount / (double)pageSize);
// Create a view model containing the paged items and pagination metadata
var viewModel = new Pagination_DTO
{
Data = pagedData,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = totalPages1
};
return Ok(viewModel);
}
catch (Exception ex)
{
// Handle exception and return a default result or an error response
return StatusCode(500, "Internal Server Error");
}
}
[HttpGet("{id:int}")]
[HttpGet("GetWorkorderById")]
public async Task<IActionResult> GetWorkorderById(int id)
{
var exist = await _db.workOrders.Include(w => w.Locations).Include(w => w.Comments).Include(w => w.Quotes).Include(w => w.Dispatches).ThenInclude(d => d.Vendor).Where(w => w.Id == id).Select(s => new
{
s.Id,
s.InternalWONumber,
s.WorkerOrderNumber,
title = s.WorkerOrderTitle,
s.Description,
s.CreatedDate,
s.DueDate,
s.ScheduledDate,
s.CompletedDate,
s.Status,
assignee = s.AssignToUser != null ? s.AssignToUser.FirstName : "",
location = s.Locations != null ? s.Locations.Name : "",
locationId = s.LocationId,
locationZip = s.Locations != null ? (s.Locations.Zip ?? ExtractZip(s.Locations.Address1)) : "",
priority = s.Priority,
s.Problem,
s.Trade,
s.SubTrade,
s.VendorNTE,
s.Source,
s.PO,
s.TT,
poc = s.WorkOrderContacts.Select(c => new { c.Id, name = c.POC != null ? (c.POC.FirstName + " " + c.POC.LastName).Trim() : "", email = c.POC != null ? c.POC.Email : "", phone = c.POC != null ? c.POC.PhoneNumber : "" }),
category = s.WorkOrderCategories.Select(c => new { c.Id, name = c.Category != null ? c.Category.Name : "" }),
Attachments = s.workOrderAttachments.Select(a => new { a.Id, attachment = a.Attachments }),
s.BeforPhotoIssue,
s.BeforPhotoAttachment,
s.AfterPhotoIssue,
s.AfterPhotoAttachment,
s.SignOffName,
s.SignOffAttachment,
s.SignOffSignature,
Comments = s.Comments.Select(c => new { c.Id, c.CreatedDate, c.Commenttext, c.Commenter, c.RecordType, c.CommentType, c.Documents, userName = c.ApplicationUser != null ? c.ApplicationUser.FirstName : "" }),
quotes = s.Quotes.Select(q => new { code = q.QuotId, q.Id }),
auditLog = _db.WorkOrderAuditLogs
.Where(a => a.WorkOrderId == s.Id)
.OrderByDescending(a => a.CreatedAt)
.Select(a => new {
a.Id,
a.FieldName,
a.OldValue,
a.NewValue,
a.Action,
a.CreatedAt,
userName = a.User != null ? a.User.FirstName + " " + a.User.LastName : ""
}).ToList(),
dispatches = s.Dispatches.Select(d => new {
d.Id,
d.DispatchNumber,
d.PONumber,
d.NTEAmount,
d.Description,
d.Status,
d.ScheduledDate,
d.CompletedDate,
d.DispatchedAt,
d.EmailSent,
vendorName = d.Vendor != null ? d.Vendor.CompanyName : "",
vendorEmail = d.Vendor != null ? d.Vendor.Email : "",
vendorId = d.VendorId
}).ToList()
}).FirstOrDefaultAsync();
if (exist == null)
{
return BadRequest(new Response { Status = "Error", Message = "ID not found!" });
}
else
{
return Ok(exist);
}
}
[HttpGet]
[Route("GetFilteredWorkorder2")]
public IActionResult GetFilteredWorkorder2([FromQuery] string[] assignto, [FromQuery] int[]? location, [FromQuery] string[]? priority, [FromQuery] string[]? status, [FromQuery] string[]? duedate, [FromQuery] string search = "", [FromQuery] string sort = "Sort Order", [FromQuery] string sortby = "Sort By", int page = 1, int pageSize = 10)
{
try
{
var totalCount = _db.workOrders.Where(w => w.istemplate != true).Count();
if (sort == "Sort Order" && sortby == "Sort By" && assignto.Count() == 0 && (duedate == null || duedate.Count() == 0) && location.Count() == 0 && (priority == null || priority.Count() == 0) && (status == null || status.Count() == 0) && search == "")
{
var data1 = _db.workOrders.Where(w => w.istemplate != true).Include(w => w.Locations).ToListAsync();
return Ok(data1);
}
//if (sort == "Sort Order" && sortby == "Sort By" && assignto.Count() == 0 && (duedate == null || duedate.Count() == 0) && location.Count() == 0 && (priority == null || priority.Count() == 0) && (status == null || status.Count() == 0) && search == "")
//{
// var data1 = _db.workOrders.Where(w => w.istemplate != true).Include(w => w.Locations).Select(s => new {s.Id, s.WorkerOrderTitle, s.DueDate, s.Status, s.Priority, s.Locations.Name }).OrderByDescending(d => d.Name).ToList().Skip((page - 1) * pageSize)
// .Take(pageSize);
// // Calculate the number of pages
// var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
// // Create a view model containing the paged items and pagination metadata
// var viewModel1 = new Pagination_DTO
// {
// Data = data1,
// PageNumber = page,
// PageSize = pageSize,
// TotalCount = totalCount,
// TotalPages = totalPages
// };
// return Ok(viewModel1);
//}
var currentDate = DateTime.UtcNow.Date;
var data = _db.workOrders.Include(w => w.Locations)
.Where(w => w.istemplate != true && (((w.WorkerOrderTitle ?? "").ToLower().Contains(search.ToLower()) || (w.WorkerOrderNumber ?? "").ToLower().Contains(search.ToLower()) || (w.Locations != null && (w.Locations.Title ?? "").ToLower().Contains(search.ToLower()))) &&
(assignto.Count() == 0 || assignto.Contains(w.AssignTo)) &&
(location.Count() == 0 || (w.LocationId.HasValue && location.Contains(w.LocationId.Value))) &&
(priority.Count() == 0 || priority.Contains(w.Priority)) &&
((status.Count() == 0 || status.Contains(w.Status))
)))
.ToList();
// Perform client-side filtering for due dates
data = data.Where(w =>
duedate.Count() == 0 ||
duedate.Any(dd => w.DueDate.HasValue && CheckDueDate(dd, w.DueDate.Value.Date, currentDate))).ToList();
// Apply sorting
if (!string.IsNullOrEmpty(sort) && !string.IsNullOrEmpty(sortby) && sortby != "")
{
switch (sortby)
{
case "Name":
data = (sort.ToLower() == "ascending") ? data.OrderBy(w => w.WorkerOrderTitle).ToList() : data.OrderByDescending(w => w.WorkerOrderTitle).ToList();
break;
case "Due Date":
data = (sort.ToLower() == "ascending") ? data.OrderBy(w => w.DueDate).ToList() : data.OrderByDescending(w => w.DueDate).ToList();
break;
case "Severity":
data = (sort.ToLower() == "ascending") ? data.OrderBy(w => w.Priority).ToList() : data.OrderByDescending(w => w.Priority).ToList();
break;
default:
// Handle default case or throw an exception
break;
}
}
var newdata = data.Select(s => new { s.Id, s.WorkerOrderTitle, s.DueDate, s.Status, s.Priority, Name = s.Locations != null ? s.Locations.Name : "" }).OrderByDescending(d => d.Name).ToList().Skip((page - 1) * pageSize)
.Take(pageSize);
var totalPages1 = (int)Math.Ceiling(totalCount / (double)pageSize);
// Create a view model containing the paged items and pagination metadata
var viewModel = new Pagination_DTO
{
Data = newdata,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = totalPages1
};
return Ok(viewModel);
}
catch (Exception ex)
{
var totalCount = _db.workOrders.Where(w => w.istemplate != true).Count();
var data1 = _db.workOrders.Where(w => w.istemplate != true).Include(w => w.Locations).Select(s => new
{
s.Id,
s.WorkerOrderTitle,
s.DueDate,
s.Status,
s.Priority,
Name = s.Locations != null ? s.Locations.Name : ""
}).OrderByDescending(d => d.Id).ToList().Skip((page - 1) * pageSize)
.Take(pageSize);
// Calculate the number of pages
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
// Create a view model containing the paged items and pagination metadata
var viewModel = new Pagination_DTO
{
Data = data1,
PageNumber = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = totalPages
};
return Ok(viewModel);
}
}
bool CheckDueDate(string dd, DateTime dueDate, DateTime currentDate)
{
return dd switch
{
"Today" => dueDate == currentDate,
"Tomorrow" => dueDate == currentDate.AddDays(1),
"Next Week" => dueDate >= currentDate.AddDays(1) && dueDate <= currentDate.AddDays(7),
"Next Month" => dueDate >= currentDate.AddDays(1) && dueDate <= currentDate.AddMonths(1),
"Over Due" => dueDate < currentDate,
_ => false
};
}
[HttpPost]
[Route("ChangeStatus")]
public async Task<IActionResult> ChangeStatus(int id, string status)
{
try
{
var 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<object>();
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,
DispatchWorkOrders = workOrders.Select(wo => new DispatchWorkOrder { WorkOrderId = wo.Id }).ToList()
};
var checklistItems = new List<DispatchChecklistItem>();
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 =>
$"<tr><td style='padding:6px 8px;border:1px solid #ddd;'>{System.Net.WebUtility.HtmlEncode(wo.InternalWONumber ?? "")}</td>" +
$"<td style='padding:6px 8px;border:1px solid #ddd;'>{System.Net.WebUtility.HtmlEncode(wo.WorkerOrderTitle ?? "")}</td>" +
$"<td style='padding:6px 8px;border:1px solid #ddd;'>{System.Net.WebUtility.HtmlEncode(wo.Priority ?? "")}</td>" +
$"<td style='padding:6px 8px;border:1px solid #ddd;'>{wo.DueDate?.ToString("yyyy-MM-dd") ?? "N/A"}</td></tr>"));
var locationName = System.Net.WebUtility.HtmlEncode(primaryWO.Locations?.Name ?? "");
var locationAddr = System.Net.WebUtility.HtmlEncode(primaryWO.Locations?.Address1 ?? "");
var subject = $"[{dispatchNumber}] {(workOrders.Count > 1 ? $"{workOrders.Count} Work Orders" : primaryWO.WorkerOrderTitle)} at {primaryWO.Locations?.Name ?? ""}";
var descriptionHtml = System.Net.WebUtility.HtmlEncode(model.Description ?? primaryWO.Description ?? "");
var senderNameHtml = System.Net.WebUtility.HtmlEncode(senderName);
var vendorToken = await _vendorTokens.GetOrCreateActiveTokenAsync(vendorId);
var frontendBase = _config.GetValue<string>("FrontendBaseUrl")?.TrimEnd('/') ?? "";
var portalUrl = $"{frontendBase}/v/{vendorToken.Token}/dashboard";
var html = $@"
<h2>Work Order Dispatch — {dispatchNumber}</h2>
<p><strong>Location:</strong> {locationName} {locationAddr}</p>
<p><strong>Vendor PO:</strong> {poNumber} &nbsp; <strong>NTE:</strong> ${model.NTEAmount?.ToString("F2") ?? "N/A"}</p>
<h3>Work Orders</h3>
<table style='border-collapse:collapse;width:100%;font-family:Arial,sans-serif;'>
<tr style='background:#f5f5f5;'>
<th style='padding:6px 8px;border:1px solid #ddd;text-align:left;'>WO #</th>
<th style='padding:6px 8px;border:1px solid #ddd;text-align:left;'>Title</th>
<th style='padding:6px 8px;border:1px solid #ddd;text-align:left;'>Priority</th>
<th style='padding:6px 8px;border:1px solid #ddd;text-align:left;'>Due Date</th>
</tr>
{woListHtml}
</table>
<h3>Description</h3>
<p>{descriptionHtml}</p>
<br/>
<div style='text-align:center;margin:20px 0;'>
<a href='{portalUrl}' style='display:inline-block;padding:12px 32px;background:#28a745;color:white;text-decoration:none;border-radius:6px;font-weight:bold;font-size:16px;'>View Work Order</a>
</div>
<br/><p style='color:#666;font-size:13px;'>{senderNameHtml}<br/>Sea Haven Industries</p>
<hr/><p style='color:#999;font-size:12px;'>Reply to this email to communicate about this dispatch.</p>";
var emailSent = await _sendMessage.SendDispatchEmail(vendor.Email, subject, html, replyTo);
dispatch.EmailSent = emailSent;
_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<IActionResult> 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<IActionResult> GetDispatchById(int id)
{
var d = await _db.Dispatches
.Include(x => x.Vendor)
.Include(x => x.WorkOrder)
.Where(x => x.Id == id)
.Select(x => new
{
x.Id,
x.DispatchNumber,
x.PONumber,
x.NTEAmount,
x.Description,
x.Status,
x.ScheduledDate,
x.CompletedDate,
x.DispatchedAt,
x.EmailSent,
x.WorkOrderId,
x.AcknowledgedAt,
x.VerifiedBy,
x.VerifiedAt,
woNumber = x.WorkOrder != null ? x.WorkOrder.InternalWONumber : "",
woTitle = x.WorkOrder != null ? x.WorkOrder.WorkerOrderTitle : "",
vendorName = x.Vendor != null ? x.Vendor.CompanyName : "",
vendorEmail = x.Vendor != null ? x.Vendor.Email : "",
vendorId = x.VendorId,
workOrders = _db.DispatchWorkOrders
.Where(dwo => dwo.DispatchId == x.Id)
.Select(dwo => new {
dwo.WorkOrderId,
woNumber = dwo.WorkOrder != null ? dwo.WorkOrder.InternalWONumber : "",
woTitle = dwo.WorkOrder != null ? dwo.WorkOrder.WorkerOrderTitle : ""
}).ToList(),
checklistItems = _db.DispatchChecklistItems
.Where(ci => ci.DispatchId == x.Id)
.OrderBy(ci => ci.SortOrder)
.Select(ci => new {
ci.Id,
ci.ItemText,
ci.IsCompleted,
ci.CompletedBy,
ci.CompletedAt,
ci.SortOrder,
ci.WorkOrderId
}).ToList(),
signoffs = _db.DispatchSignoffs
.Where(s => s.DispatchId == x.Id)
.Select(s => new {
s.Id,
s.SignoffType,
s.Name,
s.Signature,
s.SignatureMethod,
s.SignedAt
}).ToList(),
comments = _db.Comments
.Where(c => c.DispatchId == x.Id)
.OrderBy(c => c.CreatedDate)
.Select(c => new
{
c.Id,
c.CreatedDate,
c.Commenttext,
c.Commenter,
c.CommentType,
userName = c.ApplicationUser != null ? c.ApplicationUser.FirstName + " " + c.ApplicationUser.LastName : ""
}).ToList()
}).FirstOrDefaultAsync();
if (d == null)
return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
return Ok(d);
}
[HttpPost("UpdateDispatch")]
public async Task<IActionResult> UpdateDispatch([FromBody] UpdateDispatch_DTO model)
{
try
{
var dispatch = await _db.Dispatches.FindAsync(model.Id);
if (dispatch == null)
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (dispatch.Status != model.Status)
{
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
{
WorkOrderId = dispatch.WorkOrderId ?? 0,
UserId = userId,
FieldName = $"Dispatch {dispatch.DispatchNumber} Status",
OldValue = dispatch.Status ?? "",
NewValue = model.Status ?? "",
Action = "dispatch_status_change",
CreatedAt = DateTime.UtcNow
});
}
dispatch.Status = model.Status;
dispatch.NTEAmount = model.NTEAmount;
dispatch.ScheduledDate = model.ScheduledDate;
dispatch.CompletedDate = model.CompletedDate;
dispatch.Description = model.Description;
if (model.Status == "Completed" && dispatch.CompletedDate == null)
dispatch.CompletedDate = DateTime.UtcNow;
await _db.SaveChangesAsync();
return Ok(new { message = "Dispatch updated" });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPost("AddDispatchComment")]
public async Task<IActionResult> AddDispatchComment([FromBody] DispatchComment_DTO model)
{
try
{
var dispatch = await _db.Dispatches.Include(d => d.Vendor).Include(d => d.WorkOrder)
.FirstOrDefaultAsync(d => d.Id == model.DispatchId);
if (dispatch == null)
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
var userId = _httpContext?.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "";
var user = !string.IsNullOrEmpty(userId) ? await _db.Users.FindAsync(userId) : null;
var senderName = user != null ? (user.FirstName + " " + user.LastName).Trim() : "";
var comment = new Comments
{
CreatedDate = DateTime.UtcNow,
UserId = userId,
WorkerOrderId = dispatch.WorkOrderId,
DispatchId = dispatch.Id,
Commenttext = model.Text,
CommentType = "dispatcher",
Commenter = !string.IsNullOrWhiteSpace(senderName) ? senderName : null,
};
_db.Comments.Add(comment);
await _db.SaveChangesAsync();
if (model.SendEmail && dispatch.Vendor?.Email != null && dispatch.WorkOrder != null)
{
var subject = $"[WO-{dispatch.WorkOrder.InternalWONumber}-{dispatch.DispatchNumber}] {dispatch.WorkOrder.WorkerOrderTitle}";
var html = $"<p>{model.Text}</p><br/><p style='color:#666;font-size:13px;'>{senderName}<br/>Sea Haven Industries</p><hr/><p style='color:#999;font-size:12px;'>Reply to this email to respond.</p>";
await _sendMessage.SendDispatchEmail(dispatch.Vendor.Email, subject, html, dispatch.ReplyToAddress);
}
return Ok(new
{
comment.Id,
comment.CreatedDate,
comment.Commenttext,
comment.CommentType,
userName = user != null ? (user.FirstName + " " + user.LastName).Trim() : ""
});
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
private static string? ExtractZip(string? address)
{
if (string.IsNullOrWhiteSpace(address)) return null;
var match = System.Text.RegularExpressions.Regex.Match(address, @"\b(\d{5})(?:-\d{4})?\b");
return match.Success ? match.Groups[1].Value : null;
}
[HttpPost("VerifyDispatch")]
public async Task<IActionResult> VerifyDispatch(int dispatchId)
{
try
{
var dispatch = await _db.Dispatches.FindAsync(dispatchId);
if (dispatch == null)
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
var incompleteItems = await _db.DispatchChecklistItems
.Where(c => c.DispatchId == dispatchId && !c.IsCompleted)
.CountAsync();
var signoffCount = await _db.DispatchSignoffs
.Where(s => s.DispatchId == dispatchId)
.CountAsync();
var missing = new List<string>();
if (incompleteItems > 0)
missing.Add($"{incompleteItems} checklist item(s) incomplete");
if (signoffCount < 2)
{
var existingTypes = await _db.DispatchSignoffs
.Where(s => s.DispatchId == dispatchId)
.Select(s => s.SignoffType)
.ToListAsync();
if (!existingTypes.Contains("Customer")) missing.Add("Missing Customer sign-off");
if (!existingTypes.Contains("Vendor")) missing.Add("Missing Vendor sign-off");
}
if (missing.Count > 0)
return BadRequest(new { status = "Error", message = "Cannot verify", missing });
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var user = userId != null ? await _db.Users.FindAsync(userId) : null;
var userName = user != null ? (user.FirstName + " " + user.LastName).Trim() : "";
dispatch.Status = "Verified";
dispatch.VerifiedBy = userName;
dispatch.VerifiedAt = DateTime.UtcNow;
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
{
WorkOrderId = dispatch.WorkOrderId ?? 0,
UserId = userId,
FieldName = $"Dispatch {dispatch.DispatchNumber}",
OldValue = dispatch.Status ?? "",
NewValue = "Verified",
Action = "dispatch_verified",
CreatedAt = DateTime.UtcNow
});
await _db.SaveChangesAsync();
return Ok(new { message = "Dispatch verified", verifiedBy = userName, verifiedAt = dispatch.VerifiedAt });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPost("AddDispatchSignoff")]
public async Task<IActionResult> AddDispatchSignoff([FromBody] DispatchSignoff_DTO model)
{
try
{
var dispatch = await _db.Dispatches.FindAsync(model.DispatchId);
if (dispatch == null)
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
var existing = await _db.DispatchSignoffs
.AnyAsync(s => s.DispatchId == model.DispatchId && s.SignoffType == model.SignoffType);
if (existing)
return BadRequest(new Response { Status = "Error", Message = $"{model.SignoffType} sign-off already exists" });
var signoff = new DispatchSignoff
{
DispatchId = model.DispatchId,
SignoffType = model.SignoffType,
Name = model.Name,
Signature = model.Signature,
SignatureMethod = model.SignatureMethod,
SignedAt = DateTime.UtcNow
};
_db.DispatchSignoffs.Add(signoff);
await _db.SaveChangesAsync();
return Ok(new { signoff.Id, signoff.SignoffType, signoff.Name, signoff.SignedAt, signoff.SignatureMethod });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPost("UpdateChecklistItem")]
public async Task<IActionResult> UpdateChecklistItem([FromBody] ChecklistItemUpdate_DTO model)
{
var item = await _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<IActionResult> 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<string> 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; }
}
}
}