2024-09-28 14:58:36 -04:00
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 ;
2025-03-09 19:39:28 -05:00
using Microsoft.VisualBasic ;
2025-02-18 13:13:33 -06:00
using SendGrid.Helpers.Mail ;
2024-09-28 14:58:36 -04:00
using System.IO ;
using System.Net.Http.Headers ;
using System.Security.Claims ;
using static System . Runtime . InteropServices . JavaScript . JSType ;
namespace Api.SeaHavenIndustries.Controllers
{
2026-03-30 06:49:40 -05:00
// [Authorize]
2024-09-28 14:58:36 -04:00
[ApiController]
2025-02-18 13:13:33 -06:00
[Route("api/WorkOrder")]
2026-04-16 17:32:28 -04:00
[Route("api/workorders")]
2024-09-28 14:58:36 -04:00
public class WorkOrderController : Controller
{
private readonly UserManager < ApplicationUser > _userManager ;
private readonly ApplicationDbContext _db ;
private readonly IWebHostEnvironment _webHostEnvironment ;
IHttpContextAccessor _httpContext ;
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
private readonly Helper . SendMessage _sendMessage ;
public WorkOrderController ( UserManager < ApplicationUser > userManager , ApplicationDbContext db , IWebHostEnvironment webHostEnvironment , IHttpContextAccessor httpContext , Helper . SendMessage sendMessage )
2024-09-28 14:58:36 -04:00
{
_userManager = userManager ;
_db = db ;
_webHostEnvironment = webHostEnvironment ;
_httpContext = httpContext ;
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
_sendMessage = sendMessage ;
2024-09-28 14:58:36 -04:00
}
[HttpPost]
[Route("AddWorkorder")]
2025-03-09 19:39:28 -05:00
public async Task < IActionResult > Addworkorder ( [ FromBody ] Workorder_DTO model )
2024-09-28 14:58:36 -04:00
{
try
{
string userId = User . FindFirstValue ( ClaimTypes . NameIdentifier ) ;
WorkOrder workOrder = new WorkOrder ( ) ;
2026-04-16 18:42:58 -04:00
workOrder . InternalWONumber = await GenerateInternalWONumber ( ) ;
2024-09-28 14:58:36 -04:00
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 ;
2026-03-30 06:49:40 -05:00
workOrder . WorkOrderContacts = workOrder . WorkOrderContacts = = null ? new List < WorkOrderContacts > ( ) : workOrder . WorkOrderContacts ;
2024-09-28 14:58:36 -04:00
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
2026-03-30 06:49:40 -05:00
workOrder . WorkOrderCategories . Add ( new WorkOrderCategories { CategoryId = categoryId } ) ;
2024-09-28 14:58:36 -04:00
}
#region Images of workorder
workOrder . workOrderAttachments = workOrder . workOrderAttachments = = null ? new List < WorkOrderAttachments > ( ) : workOrder . workOrderAttachments ;
2025-02-18 13:13:33 -06:00
if ( model . Attachments ! = null )
2024-09-28 14:58:36 -04:00
{
2025-02-18 13:13:33 -06:00
foreach ( var attachment in model . Attachments )
{
string fileName = ContentDispositionHeaderValue . Parse ( attachment . ContentDisposition ) . FileName . Trim ( '"' ) ;
string uniqueFileName = $"{Guid.NewGuid()}_{fileName}" ;
2024-09-28 14:58:36 -04:00
2025-02-18 13:13:33 -06:00
string uploadProfilePath = Path . Combine ( "Assets" , "Documents" ) ;
string fullPath = Path . Combine ( _webHostEnvironment . WebRootPath , uploadProfilePath , uniqueFileName ) ;
2024-09-28 14:58:36 -04:00
2025-02-18 13:13:33 -06:00
Directory . CreateDirectory ( Path . GetDirectoryName ( fullPath ) ) ;
2024-09-28 14:58:36 -04:00
2025-02-18 13:13:33 -06:00
// Copy the file to the server
using ( var ms = System . IO . File . Create ( fullPath ) )
{
await attachment . CopyToAsync ( ms ) ;
}
2024-09-28 14:58:36 -04:00
2025-02-18 13:13:33 -06:00
// 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 } ) ;
}
}
2024-09-28 14:58:36 -04:00
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]
2026-04-16 17:44:00 -04:00
[HttpPut("{id:int}")]
2024-09-28 14:58:36 -04:00
[Route("EditWorkorder")]
2025-03-09 19:39:28 -05:00
public async Task < IActionResult > Editworkorder ( [ FromBody ] EditWorkorder_DTO model )
2024-09-28 14:58:36 -04:00
{
try
{
var workOrder = _db . workOrders . Where ( w = > w . Id = = model . Id ) . FirstOrDefault ( ) ;
2026-04-16 16:33:42 -04:00
if ( workOrder = = null )
return BadRequest ( new Response { Status = "Error" , Message = "Work order not found" } ) ;
2026-04-16 18:09:57 -04:00
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 ) ;
Add configurable dropdowns, new WO fields, and seed data
- Create DropdownOption model with Category, Value, ParentValue for Trade/SubTrade/Problem
- Add DropdownOptionsController with CRUD + ByCategory endpoint with parent filtering
- Add Problem, Trade, SubTrade, VendorNTE, ScheduledDate, CompletedDate, Source to WorkOrder
- Update EditWorkorder_DTO and GetWorkorderById with new fields
- Audit log tracks changes to all new fields
- Seed default Trades (10), SubTrades (20), and Problems (11) on startup
2026-04-17 10:40:38 -04:00
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 ) ;
2026-04-16 18:09:57 -04:00
2024-09-28 14:58:36 -04:00
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 ;
2026-04-16 18:09:57 -04:00
workOrder . Status = model . Status ;
Add configurable dropdowns, new WO fields, and seed data
- Create DropdownOption model with Category, Value, ParentValue for Trade/SubTrade/Problem
- Add DropdownOptionsController with CRUD + ByCategory endpoint with parent filtering
- Add Problem, Trade, SubTrade, VendorNTE, ScheduledDate, CompletedDate, Source to WorkOrder
- Update EditWorkorder_DTO and GetWorkorderById with new fields
- Audit log tracks changes to all new fields
- Seed default Trades (10), SubTrades (20), and Problems (11) on startup
2026-04-17 10:40:38 -04:00
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 ;
2024-09-28 14:58:36 -04:00
workOrder . BeforPhotoIssue = model . BeforPhotoIssue ;
workOrder . AfterPhotoIssue = model . AfterPhotoIssue ;
workOrder . SignOffName = model . SignOffName ;
workOrder . SignOffSignature = model . SignOffSignature ;
2026-03-30 06:49:40 -05:00
var existcontact = _db . WorkOrderContacts . Where ( w = > w . WorkorderId = = model . Id ) . ToList ( ) ;
if ( existcontact . Count ( ) > 0 )
2024-09-28 14:58:36 -04:00
{
_db . WorkOrderContacts . RemoveRange ( existcontact ) ;
}
2026-03-30 06:49:40 -05:00
var existcategory = _db . workOrderCategories . Where ( w = > w . WorkorderId = = model . Id ) . ToList ( ) ;
if ( existcategory . Count ( ) > 0 )
2024-09-28 14:58:36 -04:00
{
_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 ;
2026-04-16 16:33:42 -04:00
if ( model . Attachments ! = null )
2024-09-28 14:58:36 -04:00
{
2026-04-16 16:33:42 -04:00
foreach ( var attachment in model . Attachments )
{
string fileName = ContentDispositionHeaderValue . Parse ( attachment . ContentDisposition ) . FileName . Trim ( '"' ) ;
string uniqueFileName = $"{Guid.NewGuid()}_{fileName}" ;
2024-09-28 14:58:36 -04:00
2026-04-16 16:33:42 -04:00
string uploadProfilePath = Path . Combine ( "Assets" , "Documents" ) ;
string fullPath = Path . Combine ( _webHostEnvironment . WebRootPath , uploadProfilePath , uniqueFileName ) ;
2024-09-28 14:58:36 -04:00
2026-04-16 16:33:42 -04:00
Directory . CreateDirectory ( Path . GetDirectoryName ( fullPath ) ) ;
2024-09-28 14:58:36 -04:00
2026-04-16 16:33:42 -04:00
using ( var ms = System . IO . File . Create ( fullPath ) )
{
await attachment . CopyToAsync ( ms ) ;
}
2024-09-28 14:58:36 -04:00
2026-04-16 16:33:42 -04:00
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 } ) ;
}
2024-09-28 14:58:36 -04:00
}
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 } ) ;
}
}
2026-03-30 06:49:40 -05:00
// -----------------------------------
// GET: api/WorkOrder/GetWorkOrderList
// -----------------------------------
[HttpGet("GetWorkOrderList")]
2026-04-17 12:57:37 -04:00
public IActionResult GetWorkOrderList ( string? search = "" , int page = 1 , int pageSize = 12 , string? sortBy = "lastUpdated" , string? sortDir = "desc" , string? status = "" , string? assignee = "" )
2026-03-30 06:49:40 -05:00
{
search ? ? = "" ;
var query = _db . workOrders
. Include ( w = > w . Locations )
. Include ( w = > w . AssignToUser )
. Where ( w = > w . istemplate ! = true ) ;
2026-04-17 12:57:37 -04:00
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 ) ) ;
2026-03-30 06:49:40 -05:00
if ( ! string . IsNullOrWhiteSpace ( search ) )
{
var s = search . ToLower ( ) ;
query = query . Where ( w = >
2026-04-17 12:31:00 -04:00
( w . InternalWONumber ? ? "" ) . ToLower ( ) . Contains ( s ) | |
2026-03-30 06:49:40 -05:00
( 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 ( ) ;
2026-04-17 12:50:23 -04:00
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
2026-03-30 06:49:40 -05:00
. Skip ( ( page - 1 ) * pageSize )
. Take ( pageSize )
. Select ( w = > new
{
id = w . Id ,
2026-04-16 18:42:58 -04:00
internalWONumber = w . InternalWONumber ,
2026-03-30 06:49:40 -05:00
workOrderNumber = w . WorkerOrderNumber ,
2026-04-16 18:42:58 -04:00
number = w . InternalWONumber ? ? w . WorkerOrderNumber ,
2026-03-30 06:49:40 -05:00
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 : "" ,
2026-04-17 12:33:30 -04:00
employeeName = w . AssignToUser ! = null ? w . AssignToUser . FirstName + " " + w . AssignToUser . LastName : "" ,
2026-04-17 12:35:14 -04:00
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 ( )
2026-03-30 06:49:40 -05:00
} )
. ToList ( ) ;
var viewModel = new Pagination_DTO
{
Data = data ,
PageNumber = page ,
PageSize = pageSize ,
TotalCount = totalCount ,
TotalPages = ( int ) Math . Ceiling ( totalCount / ( double ) pageSize )
} ;
return Ok ( viewModel ) ;
}
2024-09-28 14:58:36 -04:00
2025-03-09 19:39:28 -05:00
[HttpDelete]
2024-09-28 14:58:36 -04:00
[Route("DeleteWorkorder")]
2025-03-09 19:39:28 -05:00
public IActionResult Deleteworkorder ( [ FromBody ] EditWorkorder_DTO model )
2024-09-28 14:58:36 -04:00
{
2025-03-09 19:39:28 -05:00
int id = model . Id . HasValue ? model . Id . Value : 0 ;
2026-03-30 06:49:40 -05:00
if ( id = = 0 )
{
2025-03-09 19:39:28 -05:00
return BadRequest ( "Id is required" ) ;
}
2024-09-28 14:58:36 -04:00
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 ) )
{
2026-04-16 16:33:42 -04:00
var s = search . ToLower ( ) ;
2024-09-28 14:58:36 -04:00
query = query . Where ( w = >
2026-04-16 16:33:42 -04:00
( w . WorkerOrderTitle ? ? "" ) . ToLower ( ) . Contains ( s ) | |
( w . WorkerOrderNumber ? ? "" ) . ToLower ( ) . Contains ( s ) | |
( w . Locations ! = null & & ( w . Locations . Title ? ? "" ) . ToLower ( ) . Contains ( s ) ) ) ;
2024-09-28 14:58:36 -04:00
}
if ( assignto ! = null & & assignto . Length > 0 )
{
2026-03-30 06:49:40 -05:00
if ( assignto [ 0 ] ! = "" & & assignto [ 0 ] ! = null )
query = query . Where ( w = > assignto . Contains ( w . AssignTo ) ) ;
2024-09-28 14:58:36 -04:00
}
if ( location ! = null & & location . Length > 0 )
{
2026-03-30 06:49:40 -05:00
if ( location [ 0 ] ! = 0 )
query = query . Where ( w = > location . Contains ( w . LocationId ? ? 0 ) ) ;
2024-09-28 14:58:36 -04:00
}
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 )
2026-04-16 16:33:42 -04:00
. Select ( s = > new { s . Id , s . WorkerOrderTitle , s . DueDate , s . Status , s . Priority , Name = s . Locations ! = null ? s . Locations . Name : "" } )
2024-09-28 14:58:36 -04:00
. 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" ) ;
}
}
2026-04-16 17:32:28 -04:00
[HttpGet("{id:int}")]
[HttpGet("GetWorkorderById")]
2024-09-28 14:58:36 -04:00
public async Task < IActionResult > GetWorkorderById ( int id )
{
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
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
2024-09-28 14:58:36 -04:00
{
s . Id ,
2026-04-16 18:42:58 -04:00
s . InternalWONumber ,
2024-09-28 14:58:36 -04:00
s . WorkerOrderNumber ,
2026-03-30 06:49:40 -05:00
title = s . WorkerOrderTitle ,
2026-04-16 17:37:21 -04:00
s . Description ,
s . CreatedDate ,
2024-09-28 14:58:36 -04:00
s . DueDate ,
Add configurable dropdowns, new WO fields, and seed data
- Create DropdownOption model with Category, Value, ParentValue for Trade/SubTrade/Problem
- Add DropdownOptionsController with CRUD + ByCategory endpoint with parent filtering
- Add Problem, Trade, SubTrade, VendorNTE, ScheduledDate, CompletedDate, Source to WorkOrder
- Update EditWorkorder_DTO and GetWorkorderById with new fields
- Audit log tracks changes to all new fields
- Seed default Trades (10), SubTrades (20), and Problems (11) on startup
2026-04-17 10:40:38 -04:00
s . ScheduledDate ,
s . CompletedDate ,
2024-09-28 14:58:36 -04:00
s . Status ,
2026-04-16 16:33:42 -04:00
assignee = s . AssignToUser ! = null ? s . AssignToUser . FirstName : "" ,
location = s . Locations ! = null ? s . Locations . Name : "" ,
2026-04-16 17:37:21 -04:00
locationId = s . LocationId ,
2026-04-17 11:59:34 -04:00
locationZip = s . Locations ! = null ? ( s . Locations . ZipCode ? ? ExtractZip ( s . Locations . Address ) ) : "" ,
2024-09-28 14:58:36 -04:00
priority = s . Priority ,
Add configurable dropdowns, new WO fields, and seed data
- Create DropdownOption model with Category, Value, ParentValue for Trade/SubTrade/Problem
- Add DropdownOptionsController with CRUD + ByCategory endpoint with parent filtering
- Add Problem, Trade, SubTrade, VendorNTE, ScheduledDate, CompletedDate, Source to WorkOrder
- Update EditWorkorder_DTO and GetWorkorderById with new fields
- Audit log tracks changes to all new fields
- Seed default Trades (10), SubTrades (20), and Problems (11) on startup
2026-04-17 10:40:38 -04:00
s . Problem ,
s . Trade ,
s . SubTrade ,
s . VendorNTE ,
s . Source ,
2024-09-28 14:58:36 -04:00
s . PO ,
s . TT ,
2026-04-16 16:33:42 -04:00
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 } ) ,
2024-09-28 14:58:36 -04:00
s . BeforPhotoIssue ,
s . BeforPhotoAttachment ,
s . AfterPhotoIssue ,
s . AfterPhotoAttachment ,
s . SignOffName ,
s . SignOffAttachment ,
s . SignOffSignature ,
2026-04-16 18:09:57 -04:00
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 : ""
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
} ) . ToList ( ) ,
dispatches = s . Dispatches . Select ( d = > new {
d . Id ,
2026-04-17 14:01:03 -04:00
d . DispatchNumber ,
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
d . PONumber ,
d . NTEAmount ,
d . Description ,
d . Status ,
2026-04-17 11:47:00 -04:00
d . ScheduledDate ,
2026-04-17 14:01:03 -04:00
d . CompletedDate ,
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
d . DispatchedAt ,
d . EmailSent ,
vendorName = d . Vendor ! = null ? d . Vendor . CompanyName : "" ,
2026-04-17 14:01:03 -04:00
vendorEmail = d . Vendor ! = null ? d . Vendor . Email : "" ,
vendorId = d . VendorId
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
} ) . ToList ( )
2024-09-28 14:58:36 -04:00
} ) . FirstOrDefaultAsync ( ) ;
if ( exist = = null )
{
return BadRequest ( new Response { Status = "Error" , Message = "ID not found!" } ) ;
}
else
{
return Ok ( exist ) ;
}
}
[HttpGet]
[Route("GetFilteredWorkorder2")]
2026-03-30 06:49:40 -05:00
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 )
2024-09-28 14:58:36 -04:00
{
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 )
2026-04-16 16:33:42 -04:00
. 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 ( ) ) ) ) & &
2024-09-28 14:58:36 -04:00
( 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
2026-03-30 06:49:40 -05:00
data = data . Where ( w = >
duedate . Count ( ) = = 0 | |
duedate . Any ( dd = > w . DueDate . HasValue & & CheckDueDate ( dd , w . DueDate . Value . Date , currentDate ) ) ) . ToList ( ) ;
2024-09-28 14:58:36 -04:00
// 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 ;
}
}
2026-04-16 16:33:42 -04:00
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 )
2024-09-28 14:58:36 -04:00
. 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 ( ) ;
2026-03-30 06:49:40 -05:00
var data1 = _db . workOrders . Where ( w = > w . istemplate ! = true ) . Include ( w = > w . Locations ) . Select ( s = > new
2024-09-28 14:58:36 -04:00
{
s . Id ,
s . WorkerOrderTitle ,
s . DueDate ,
s . Status ,
s . Priority ,
2026-04-16 16:33:42 -04:00
Name = s . Locations ! = null ? s . Locations . Name : ""
2024-09-28 14:58:36 -04:00
} ) . 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 ( ) ;
2026-04-16 16:33:42 -04:00
if ( data = = null )
return BadRequest ( new Response { Status = "Error" , Message = "Work order not found" } ) ;
2026-04-16 18:09:57 -04:00
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
} ) ;
}
2024-09-28 14:58:36 -04:00
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 } ) ;
}
}
2026-04-16 19:04:59 -04:00
[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 } ) ;
}
}
2024-09-28 14:58:36 -04:00
[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 ;
2026-04-16 18:09:57 -04:00
comments . CommentType = model . CommentType ? ? "vendor" ;
2024-09-28 14:58:36 -04:00
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 } ) ;
}
}
2026-04-16 18:22:34 -04:00
[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 } ) ;
}
}
2024-09-28 14:58:36 -04:00
[HttpGet]
[Route("GetComments")]
public async Task < IActionResult > GetComments ( )
{
var data = await _db . Comments . Include ( i = > i . ApplicationUser ) . Select ( s = > new
{
s . Id ,
s . Commenttext ,
2026-04-16 16:33:42 -04:00
FirstName = s . ApplicationUser ! = null ? s . ApplicationUser . FirstName : "" ,
2024-09-28 14:58:36 -04:00
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 ,
2026-04-16 16:33:42 -04:00
FirstName = s . ApplicationUser ! = null ? s . ApplicationUser . FirstName : "" ,
2024-09-28 14:58:36 -04:00
s . Documents
} ) . ToListAsync ( ) ;
return Ok ( data ) ;
}
2025-02-18 13:13:33 -06:00
[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 ,
2026-04-16 16:33:42 -04:00
AssignToUserName = s . AssignToUser ! = null ? s . AssignToUser . FirstName : "" ,
LocationName = s . Locations ! = null ? s . Locations . Name : "" ,
2025-02-18 13:13:33 -06:00
LocationId = s . LocationId ,
PO = s . PO ,
TT = s . TT ,
} ) . ToListAsync ( ) ;
return Ok ( data ) ;
}
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
[HttpPost]
[Route("DispatchToVendor")]
public async Task < IActionResult > DispatchToVendor ( [ FromBody ] Dispatch_DTO model )
{
try
{
2026-04-17 15:16:28 -04:00
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 ( ) ;
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
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 ;
2026-04-17 14:01:03 -04:00
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 ;
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
var userId = User . FindFirstValue ( ClaimTypes . NameIdentifier ) ;
2026-04-17 14:01:03 -04:00
var currentUser = userId ! = null ? await _db . Users . FindAsync ( userId ) : null ;
var senderName = currentUser ! = null ? ( currentUser . FirstName + " " + currentUser . LastName ) . Trim ( ) : "" ;
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
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 + + ;
2026-04-17 14:01:03 -04:00
var dispatchNumber = $"DSP-{nextDSP:D5}" ;
nextDSP + + ;
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
2026-04-17 15:16:28 -04:00
var replyTo = $"wo-{primaryWO.InternalWONumber}-{dispatchNumber}@int.seahaven.com" ;
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
var dispatch = new Dispatch
{
2026-04-17 15:16:28 -04:00
WorkOrderId = primaryWO . Id ,
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
VendorId = vendorId ,
2026-04-17 14:01:03 -04:00
DispatchNumber = dispatchNumber ,
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
PONumber = poNumber ,
NTEAmount = model . NTEAmount ,
Description = model . Description ,
Status = "Sent" ,
2026-04-17 11:47:00 -04:00
ScheduledDate = model . ScheduledDate ,
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
DispatchedAt = DateTime . UtcNow ,
2026-04-17 15:16:28 -04:00
ReplyToAddress = replyTo ,
AcceptToken = Guid . NewGuid ( ) . ToString ( ) ,
DispatchWorkOrders = workOrders . Select ( wo = > new DispatchWorkOrder { WorkOrderId = wo . Id } ) . ToList ( )
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
} ;
2026-04-17 15:16:28 -04:00
var woListHtml = string . Join ( "" , workOrders . Select ( wo = >
$"<tr><td style='padding:6px 8px;border:1px solid #ddd;'>{wo.InternalWONumber}</td>" +
$"<td style='padding:6px 8px;border:1px solid #ddd;'>{wo.WorkerOrderTitle}</td>" +
$"<td style='padding:6px 8px;border:1px solid #ddd;'>{wo.Priority}</td>" +
$"<td style='padding:6px 8px;border:1px solid #ddd;'>{wo.DueDate?.ToString(" yyyy - MM - dd ") ?? " N / A "}</td></tr>" ) ) ;
var locationName = primaryWO . Locations ? . Name ? ? "" ;
var locationAddr = primaryWO . Locations ? . Address ? ? "" ;
var subject = $"[{dispatchNumber}] {(workOrders.Count > 1 ? $" { workOrders . Count } Work Orders " : primaryWO.WorkerOrderTitle)} at {locationName}" ;
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
var html = $ @ "
2026-04-17 15:16:28 -04:00
< 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 >
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
< table style = ' border - collapse : collapse ; width : 100 % ; font - family : Arial , sans - serif ; ' >
2026-04-17 15:16:28 -04:00
< tr style = ' background : # f5f5f5 ; ' >
< th style = ' padding : 6 px 8 px ; border : 1 px solid # ddd ; text - align : left ; ' > WO # < / th >
< th style = ' padding : 6 px 8 px ; border : 1 px solid # ddd ; text - align : left ; ' > Title < / th >
< th style = ' padding : 6 px 8 px ; border : 1 px solid # ddd ; text - align : left ; ' > Priority < / th >
< th style = ' padding : 6 px 8 px ; border : 1 px solid # ddd ; text - align : left ; ' > Due Date < / th >
< / tr >
{ woListHtml }
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
< / table >
< h3 > Description < / h3 >
2026-04-17 15:16:28 -04:00
< p > { model . Description ? ? primaryWO . Description } < / p >
2026-04-17 14:01:03 -04:00
< br / > < p style = ' color : # 666 ; font - size : 13 px ; ' > { senderName } < br / > Sea Haven Industries < / p >
2026-04-17 15:16:28 -04:00
< hr / > < p style = ' color : # 999 ; font - size : 12 px ; ' > Reply to this email to communicate about this dispatch . < / p > ";
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
var emailSent = await _sendMessage . SendDispatchEmail ( vendor . Email , subject , html , replyTo ) ;
dispatch . EmailSent = emailSent ;
_db . Dispatches . Add ( dispatch ) ;
2026-04-17 15:16:28 -04:00
foreach ( var wo in workOrders )
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
{
2026-04-17 15:16:28 -04:00
_db . WorkOrderAuditLogs . Add ( new WorkOrderAuditLog
{
WorkOrderId = wo . Id ,
UserId = userId ,
FieldName = "Dispatch" ,
OldValue = "" ,
NewValue = $"{vendor.CompanyName} ({dispatchNumber})" ,
Action = "dispatch" ,
CreatedAt = DateTime . UtcNow
} ) ;
}
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
2026-04-17 15:16:28 -04:00
dispatches . Add ( new { dispatch . Id , dispatchNumber , poNumber , vendorName = vendor . CompanyName , dispatch . Status , workOrderCount = workOrders . Count } ) ;
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
}
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 )
{
2026-04-17 15:16:28 -04:00
var dispatchIds = await _db . DispatchWorkOrders
. Where ( dwo = > dwo . WorkOrderId = = workOrderId )
. Select ( dwo = > dwo . DispatchId )
. Distinct ( )
. ToListAsync ( ) ;
var directIds = await _db . Dispatches
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
. Where ( d = > d . WorkOrderId = = workOrderId )
2026-04-17 15:16:28 -04:00
. Select ( d = > d . Id )
. ToListAsync ( ) ;
var allIds = dispatchIds . Union ( directIds ) . Distinct ( ) . ToList ( ) ;
var data = await _db . Dispatches
. Where ( d = > allIds . Contains ( d . Id ) )
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
. Include ( d = > d . Vendor )
. OrderByDescending ( d = > d . DispatchedAt )
. Select ( d = > new
{
d . Id ,
2026-04-17 14:01:03 -04:00
d . DispatchNumber ,
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
d . PONumber ,
d . NTEAmount ,
d . Description ,
d . Status ,
2026-04-17 14:01:03 -04:00
d . ScheduledDate ,
d . CompletedDate ,
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
d . DispatchedAt ,
d . EmailSent ,
vendorName = d . Vendor ! = null ? d . Vendor . CompanyName : "" ,
2026-04-17 14:01:03 -04:00
vendorEmail = d . Vendor ! = null ? d . Vendor . Email : "" ,
vendorId = d . VendorId
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
} )
. ToListAsync ( ) ;
return Ok ( data ) ;
}
2026-04-17 14:01:03 -04:00
[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 ,
2026-04-17 15:16:28 -04:00
x . AcknowledgedAt ,
2026-04-17 14:01:03 -04:00
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 ,
2026-04-17 15:16:28 -04:00
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 ( ) ,
2026-04-17 14:01:03 -04:00
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
{
2026-04-17 15:16:28 -04:00
WorkOrderId = dispatch . WorkOrderId ? ? 0 ,
2026-04-17 14:01:03 -04:00
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 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 = $"<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 } ) ;
}
}
2026-04-17 11:59:34 -04:00
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 ;
}
2026-04-16 18:42:58 -04:00
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" ) ;
}
2025-02-18 13:13:33 -06:00
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 ; }
2025-03-09 19:39:28 -05:00
public string FormattedDueDate
{
2026-03-30 06:49:40 -05:00
get { return DueDate . HasValue ? DueDate . Value . ToString ( "MM/dd/yyyy" ) : "" ; }
2025-03-09 19:39:28 -05:00
}
2025-02-18 13:13:33 -06:00
public string? Dispatcher { get ; set ; }
public string? Scheduler { get ; set ; }
public string? PO { get ; set ; }
public string? TT { get ; set ; }
}
2024-09-28 14:58:36 -04:00
}
}