2024-09-28 14:58:36 -04:00
using Api.SeaHavenIndustries.DTOs ;
using Data.SeaHavenIndustries ;
2026-05-14 11:00:12 -05:00
using Data.SeaHavenIndustries.Enums ;
using FluentValidation ;
2024-09-28 14:58:36 -04:00
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 ;
2026-05-14 11:00:12 -05:00
using SeaHaven.Services.Interfaces ;
using SeaHaven.Services.DTOs ;
2026-07-07 11:26:13 -03:00
using SeaHaven.Services.Exceptions ;
2026-05-06 10:49:33 -05:00
using SeaHaven.DataServices.Interfaces ;
2024-09-28 14:58:36 -04:00
namespace Api.SeaHavenIndustries.Controllers
{
2026-07-07 13:11:22 -03: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 ;
2026-05-14 11:00:12 -05:00
private readonly IWorkOrderService _workOrderService ;
2026-05-06 10:49:33 -05:00
private readonly IWorkOrderDataService _workOrderDataService ;
2026-05-14 11:00:12 -05:00
private readonly ICommentDataService _commentDataService ;
private readonly IQuotesDataService _quotesDataService ;
private readonly IUserDataService _userDataService ;
private readonly IDispatchDataService _dispatchDataService ;
private readonly IVendorDataService _vendorDataService ;
private readonly ITaskListTemplateDataService _taskListTemplateDataService ;
private readonly ApplicationDbContext _db ;
2024-09-28 14:58:36 -04:00
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 ;
2026-04-20 12:01:53 -04:00
private readonly Helper . VendorPortalTokenService _vendorTokens ;
private readonly IConfiguration _config ;
2026-07-07 10:10:06 -03:00
private readonly IWorkOrderBoardService _workOrderBoardService ;
2026-07-07 11:26:13 -03:00
private readonly IWorkOrderBoardUpdateService _boardUpdateService ;
2026-07-08 10:17:31 -03:00
private readonly IWorkOrderBoardCreateService _boardCreateService ;
private readonly IWorkOrderBoardCancelService _boardCancelService ;
2026-07-08 14:58:59 -03:00
private readonly IWorkOrderAdvancedSearchService _advancedSearchService ;
2026-05-06 10:49:33 -05:00
public WorkOrderController (
UserManager < ApplicationUser > userManager ,
2026-05-14 11:00:12 -05:00
IWorkOrderService workOrderService ,
2026-05-06 10:49:33 -05:00
IWorkOrderDataService workOrderDataService ,
2026-05-14 11:00:12 -05:00
ICommentDataService commentDataService ,
IQuotesDataService quotesDataService ,
IUserDataService userDataService ,
IDispatchDataService dispatchDataService ,
IVendorDataService vendorDataService ,
ITaskListTemplateDataService taskListTemplateDataService ,
2026-05-06 10:49:33 -05:00
ApplicationDbContext db ,
IWebHostEnvironment webHostEnvironment ,
IHttpContextAccessor httpContext ,
Helper . SendMessage sendMessage ,
Helper . VendorPortalTokenService vendorTokens ,
2026-07-07 11:26:13 -03:00
IConfiguration config ,
2026-07-07 13:59:22 -03:00
IWorkOrderBoardService workOrderBoardService ,
2026-07-08 10:17:31 -03:00
IWorkOrderBoardUpdateService boardUpdateService ,
IWorkOrderBoardCreateService boardCreateService ,
2026-07-08 14:58:59 -03:00
IWorkOrderBoardCancelService boardCancelService ,
IWorkOrderAdvancedSearchService advancedSearchService )
2024-09-28 14:58:36 -04:00
{
_userManager = userManager ;
2026-05-14 11:00:12 -05:00
_workOrderService = workOrderService ;
2026-05-06 10:49:33 -05:00
_workOrderDataService = workOrderDataService ;
2026-05-14 11:00:12 -05:00
_commentDataService = commentDataService ;
_quotesDataService = quotesDataService ;
_userDataService = userDataService ;
_dispatchDataService = dispatchDataService ;
_vendorDataService = vendorDataService ;
_taskListTemplateDataService = taskListTemplateDataService ;
2024-09-28 14:58:36 -04:00
_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 ;
2026-04-20 12:01:53 -04:00
_vendorTokens = vendorTokens ;
_config = config ;
2026-07-07 10:10:06 -03:00
_workOrderBoardService = workOrderBoardService ;
2026-07-07 11:26:13 -03:00
_boardUpdateService = boardUpdateService ;
2026-07-08 10:17:31 -03:00
_boardCreateService = boardCreateService ;
_boardCancelService = boardCancelService ;
2026-07-08 14:58:59 -03:00
_advancedSearchService = advancedSearchService ;
2024-09-28 14:58:36 -04:00
}
2026-07-07 11:26:13 -03:00
2026-07-07 10:10:06 -03:00
[HttpGet("board")]
public async Task < IActionResult > GetBoard (
[FromQuery] DateOnly weekStart ,
[FromQuery] DateOnly ? weekEnd = null ,
[FromQuery] List < string > ? dispatchers = null ,
[FromQuery] bool myWorkOrders = false ,
[FromQuery] List < WorkOrderType > ? types = null ,
[FromQuery] string? search = null )
{
var userId = User . FindFirstValue ( ClaimTypes . NameIdentifier ) ;
var query = new WorkOrderBoardQueryDto
{
WeekStart = weekStart ,
WeekEnd = weekEnd ,
Dispatchers = dispatchers ,
MyWorkOrders = myWorkOrders ,
Types = types ,
Search = search
} ;
var result = await _workOrderBoardService . GetBoardAsync ( query , userId ) ;
return Ok ( result ) ;
}
2026-07-08 14:58:59 -03:00
[HttpGet("board/search")]
public async Task < IActionResult > SearchBoard ( [ FromQuery ] WorkOrderAdvancedSearchQueryDto query )
{
2026-07-10 10:56:18 -03:00
try
{
var userId = User . FindFirstValue ( ClaimTypes . NameIdentifier ) ;
var result = await _advancedSearchService . SearchAsync ( query , userId ) ;
return Ok ( result ) ;
}
catch ( ArgumentException ex )
{
return BadRequest ( new Response { Status = "Error" , Message = ex . Message } ) ;
}
2026-07-08 14:58:59 -03:00
}
2026-07-07 10:10:06 -03:00
[HttpGet("lookups/dispatchers")]
public async Task < IActionResult > GetDispatcherLookups ( )
{
var result = await _workOrderBoardService . GetDispatcherLookupsAsync ( ) ;
return Ok ( result ) ;
}
2026-07-07 11:26:13 -03:00
[HttpPatch("{id:int}/board")]
public async Task < IActionResult > PatchBoardField ( int id , [ FromBody ] WorkOrderBoardPatchRequestDto request )
{
try
{
var actorId = User . FindFirstValue ( ClaimTypes . NameIdentifier ) ;
var row = await _boardUpdateService . PatchFieldAsync ( id , request , actorId ) ;
return Ok ( row ) ;
}
catch ( WorkOrderBoardConcurrencyException ex )
{
return Conflict ( new WorkOrderBoardConflictDto
{
CurrentState = ex . CurrentState
} ) ;
}
catch ( WorkOrderBoardValidationException ex )
{
return UnprocessableEntity ( new WorkOrderBoardValidationErrorDto
{
Code = ex . Code ,
Message = ex . Message
} ) ;
}
catch ( ArgumentException ex )
{
return BadRequest ( new Response { Status = "Error" , Message = ex . Message } ) ;
}
}
2026-07-08 10:17:31 -03:00
[HttpPost("board")]
public async Task < IActionResult > CreateBoardWorkOrder ( [ FromBody ] WorkOrderBoardCreateRequestDto request )
{
try
{
var actorId = User . FindFirstValue ( ClaimTypes . NameIdentifier ) ;
var row = await _boardCreateService . CreateAsync ( request , actorId ) ;
return Ok ( row ) ;
}
catch ( ValidationException vex )
{
var errors = string . Join ( ", " , vex . Errors . Select ( e = > e . ErrorMessage ) ) ;
return BadRequest ( new Response { Status = "Validation Error" , Message = errors } ) ;
}
catch ( WorkOrderBoardValidationException ex ) when ( ex . Code = = "DuplicateWoNumber" )
{
return Conflict ( new WorkOrderBoardValidationErrorDto
{
Code = ex . Code ,
Message = ex . Message
} ) ;
}
catch ( WorkOrderBoardValidationException ex )
{
return UnprocessableEntity ( new WorkOrderBoardValidationErrorDto
{
Code = ex . Code ,
Message = ex . Message
} ) ;
}
}
[HttpPost("{id:int}/cancel")]
public async Task < IActionResult > CancelBoardWorkOrder ( int id )
{
try
{
var actorId = User . FindFirstValue ( ClaimTypes . NameIdentifier ) ;
var row = await _boardCancelService . CancelAsync ( id , actorId ) ;
return Ok ( row ) ;
}
catch ( WorkOrderBoardValidationException ex )
{
return UnprocessableEntity ( new WorkOrderBoardValidationErrorDto
{
Code = ex . Code ,
Message = ex . Message
} ) ;
}
}
2024-09-28 14:58:36 -04:00
[HttpPost]
[Route("AddWorkorder")]
2026-05-14 11:00:12 -05:00
public async Task < IActionResult > Addworkorder ( [ FromForm ] Workorder_DTO model )
2024-09-28 14:58:36 -04:00
{
try
{
2026-05-14 11:00:12 -05:00
var userId = User . FindFirstValue ( ClaimTypes . NameIdentifier ) ;
var createDto = new CreateWorkOrderDTO
2024-09-28 14:58:36 -04:00
{
2026-05-14 11:00:12 -05:00
WorkerOrderTitle = model . WorkerOrderTitle ? ? "" ,
Description = model . Description ,
Status = WorkOrderStatus . Open ,
Priority = model . Priority ? ? WorkOrderPriority . Medium ,
LocationId = model . LocationId ,
AssignTo = model . AssignTo ,
DueDate = model . DueDate
} ;
var workOrderId = await _workOrderService . CreateWorkOrderAsync ( createDto , userId ! ) ;
if ( model . ContactIds ! = null & & model . ContactIds . Any ( ) )
{
await _workOrderDataService . AddWorkOrderContactsAsync ( workOrderId , model . ContactIds ) ;
2024-09-28 14:58:36 -04:00
}
2026-05-14 11:00:12 -05:00
if ( model . CategoryIds ! = null & & model . CategoryIds . Any ( ) )
2024-09-28 14:58:36 -04:00
{
2026-05-14 11:00:12 -05:00
await _workOrderDataService . AddWorkOrderCategoriesAsync ( workOrderId , model . CategoryIds ) ;
2024-09-28 14:58:36 -04:00
}
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 )
{
2026-05-14 11:00:12 -05:00
var fileUrl = await SaveFileAsync ( attachment ) ;
await _workOrderDataService . AddWorkOrderAttachmentAsync ( new WorkOrderAttachments
2025-02-18 13:13:33 -06:00
{
2026-05-14 11:00:12 -05:00
WorkorderId = workOrderId ,
Attachments = fileUrl
} ) ;
2025-02-18 13:13:33 -06:00
}
}
2024-09-28 14:58:36 -04:00
if ( model . BeforPhotoAttachment ! = null )
{
2026-05-14 11:00:12 -05:00
var fileUrl = await SaveFileAsync ( model . BeforPhotoAttachment ) ;
var workOrder = await _workOrderDataService . GetByIdAsync ( workOrderId ) ;
if ( workOrder ! = null )
2024-09-28 14:58:36 -04:00
{
2026-05-14 11:00:12 -05:00
workOrder . BeforPhotoAttachment = fileUrl ;
await _workOrderDataService . UpdateAsync ( workOrder ) ;
2024-09-28 14:58:36 -04:00
}
}
2026-05-14 11:00:12 -05:00
2024-09-28 14:58:36 -04:00
if ( model . AfterPhotoAttachment ! = null )
{
2026-05-14 11:00:12 -05:00
var fileUrl = await SaveFileAsync ( model . AfterPhotoAttachment ) ;
var workOrder = await _workOrderDataService . GetByIdAsync ( workOrderId ) ;
if ( workOrder ! = null )
2024-09-28 14:58:36 -04:00
{
2026-05-14 11:00:12 -05:00
workOrder . AfterPhotoAttachment = fileUrl ;
await _workOrderDataService . UpdateAsync ( workOrder ) ;
2024-09-28 14:58:36 -04:00
}
}
2026-05-14 11:00:12 -05:00
2024-09-28 14:58:36 -04:00
if ( model . SignOffAttachment ! = null )
{
2026-05-14 11:00:12 -05:00
var fileUrl = await SaveFileAsync ( model . SignOffAttachment ) ;
var workOrder = await _workOrderDataService . GetByIdAsync ( workOrderId ) ;
if ( workOrder ! = null )
2024-09-28 14:58:36 -04:00
{
2026-05-14 11:00:12 -05:00
workOrder . SignOffAttachment = fileUrl ;
await _workOrderDataService . UpdateAsync ( workOrder ) ;
2024-09-28 14:58:36 -04:00
}
}
2026-05-14 11:00:12 -05:00
return Ok ( new DataResponse { Message = "Work order created successfully" , Status = "200" , Data = workOrderId } ) ;
}
catch ( ValidationException vex )
{
var errors = string . Join ( ", " , vex . Errors . Select ( e = > e . ErrorMessage ) ) ;
return BadRequest ( new Response { Status = "Validation Error" , Message = errors } ) ;
2024-09-28 14:58:36 -04:00
}
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")]
2026-05-14 11:00:12 -05:00
public async Task < IActionResult > Editworkorder ( [ FromForm ] EditWorkorder_DTO model )
2024-09-28 14:58:36 -04:00
{
try
{
2026-05-14 11:00:12 -05:00
if ( ! model . Id . HasValue )
return BadRequest ( new Response { Status = "Error" , Message = "Work order ID is required" } ) ;
2026-04-16 18:09:57 -04:00
var userId = User . FindFirstValue ( ClaimTypes . NameIdentifier ) ;
2026-05-14 11:00:12 -05:00
var updateDto = new UpdateWorkOrderDTO
2024-09-28 14:58:36 -04:00
{
2026-05-14 11:00:12 -05:00
WorkerOrderTitle = model . WorkerOrderTitle ,
Description = model . Description ,
Status = model . Status ,
Priority = model . Priority ,
LocationId = model . LocationId ,
AssignTo = model . AssignTo ,
DueDate = model . DueDate ,
CompletedDate = model . CompletedDate
} ;
2024-09-28 14:58:36 -04:00
2026-05-14 11:00:12 -05:00
var success = await _workOrderService . UpdateWorkOrderAsync ( model . Id . Value , updateDto , userId ! ) ;
if ( ! success )
return BadRequest ( new Response { Status = "Error" , Message = "Work order not found" } ) ;
await _workOrderDataService . DeleteWorkOrderContactsAsync ( model . Id . Value ) ;
await _workOrderDataService . DeleteWorkOrderCategoriesAsync ( model . Id . Value ) ;
2024-09-28 14:58:36 -04:00
2026-05-14 11:00:12 -05:00
if ( model . ContactIds ! = null & & model . ContactIds . Any ( ) )
2024-09-28 14:58:36 -04:00
{
2026-05-14 11:00:12 -05:00
await _workOrderDataService . AddWorkOrderContactsAsync ( model . Id . Value , model . ContactIds ) ;
2024-09-28 14:58:36 -04:00
}
2026-05-14 11:00:12 -05:00
if ( model . CategoryIds ! = null & & model . CategoryIds . Any ( ) )
2024-09-28 14:58:36 -04:00
{
2026-05-14 11:00:12 -05:00
await _workOrderDataService . AddWorkOrderCategoriesAsync ( model . Id . Value , model . CategoryIds ) ;
2024-09-28 14:58:36 -04:00
}
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 )
{
2026-05-14 11:00:12 -05:00
var fileUrl = await SaveFileAsync ( attachment ) ;
await _workOrderDataService . AddWorkOrderAttachmentAsync ( new WorkOrderAttachments
2026-04-16 16:33:42 -04:00
{
2026-05-14 11:00:12 -05:00
WorkorderId = model . Id . Value ,
Attachments = fileUrl
} ) ;
2026-04-16 16:33:42 -04:00
}
2024-09-28 14:58:36 -04:00
}
if ( model . BeforPhotoAttachment ! = null )
{
2026-05-14 11:00:12 -05:00
var fileUrl = await SaveFileAsync ( model . BeforPhotoAttachment ) ;
var workOrder = await _workOrderDataService . GetByIdAsync ( model . Id . Value ) ;
if ( workOrder ! = null )
2024-09-28 14:58:36 -04:00
{
2026-05-14 11:00:12 -05:00
workOrder . BeforPhotoAttachment = fileUrl ;
await _workOrderDataService . UpdateAsync ( workOrder ) ;
2024-09-28 14:58:36 -04:00
}
}
2026-05-14 11:00:12 -05:00
2024-09-28 14:58:36 -04:00
if ( model . AfterPhotoAttachment ! = null )
{
2026-05-14 11:00:12 -05:00
var fileUrl = await SaveFileAsync ( model . AfterPhotoAttachment ) ;
var workOrder = await _workOrderDataService . GetByIdAsync ( model . Id . Value ) ;
if ( workOrder ! = null )
2024-09-28 14:58:36 -04:00
{
2026-05-14 11:00:12 -05:00
workOrder . AfterPhotoAttachment = fileUrl ;
await _workOrderDataService . UpdateAsync ( workOrder ) ;
2024-09-28 14:58:36 -04:00
}
}
2026-05-14 11:00:12 -05:00
2024-09-28 14:58:36 -04:00
if ( model . SignOffAttachment ! = null )
{
2026-05-14 11:00:12 -05:00
var fileUrl = await SaveFileAsync ( model . SignOffAttachment ) ;
var workOrder = await _workOrderDataService . GetByIdAsync ( model . Id . Value ) ;
if ( workOrder ! = null )
2024-09-28 14:58:36 -04:00
{
2026-05-14 11:00:12 -05:00
workOrder . SignOffAttachment = fileUrl ;
await _workOrderDataService . UpdateAsync ( workOrder ) ;
2024-09-28 14:58:36 -04:00
}
}
2026-05-14 11:00:12 -05:00
2024-09-28 14:58:36 -04:00
return Ok ( new DataResponse { Message = "Updated Successfully" , Status = "200" } ) ;
}
2026-05-14 11:00:12 -05:00
catch ( ValidationException vex )
{
var errors = string . Join ( ", " , vex . Errors . Select ( e = > e . ErrorMessage ) ) ;
return BadRequest ( new Response { Status = "Validation Error" , Message = errors } ) ;
}
2024-09-28 14:58:36 -04:00
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-05-06 10:49:33 -05:00
public async Task < IActionResult > GetWorkOrderList ( string? search = "" , int page = 1 , int pageSize = 12 , string? sortBy = "lastUpdated" , string? sortDir = "desc" , string? status = "" , string? assignee = "" , int? locationId = null )
2026-03-30 06:49:40 -05:00
{
2026-05-06 10:49:33 -05:00
var ( data , totalCount ) = await _workOrderDataService . GetWorkOrderListPagedAsync (
page ,
pageSize ,
search ,
status ,
assignee ,
locationId ,
sortBy ,
sortDir ) ;
2026-03-30 06:49:40 -05:00
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
2026-07-08 10:17:31 -03:00
[Authorize(Roles = "Admin")]
2025-03-09 19:39:28 -05:00
[HttpDelete]
2024-09-28 14:58:36 -04:00
[Route("DeleteWorkorder")]
2026-05-14 11:00:12 -05:00
public async Task < 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
{
2026-05-14 11:00:12 -05:00
var userId = User . FindFirstValue ( ClaimTypes . NameIdentifier ) ;
var success = await _workOrderService . DeleteWorkOrderAsync ( id , userId ! ) ;
2024-09-28 14:58:36 -04:00
2026-05-14 11:00:12 -05:00
if ( ! success )
return BadRequest ( "Work order not found!" ) ;
await _quotesDataService . UnlinkFromWorkOrderAsync ( id ) ;
var comments = await _commentDataService . GetByWorkOrderIdAsync ( id ) ;
foreach ( var comment in comments )
2024-09-28 14:58:36 -04:00
{
2026-05-14 11:00:12 -05:00
await _commentDataService . DeleteAsync ( comment . Id ) ;
2024-09-28 14:58:36 -04:00
}
2026-05-14 11:00:12 -05:00
2024-09-28 14:58:36 -04:00
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-28 18:55:14 -05:00
locationZip = s . Locations ! = null ? ( s . Locations . Zip ? ? ExtractZip ( s . Locations . Address1 ) ) : "" ,
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
{
2026-05-14 11:00:12 -05:00
var workOrder = await _workOrderDataService . GetByIdAsync ( id ) ;
if ( workOrder = = null )
2026-04-16 16:33:42 -04:00
return BadRequest ( new Response { Status = "Error" , Message = "Work order not found" } ) ;
2026-05-14 11:00:12 -05:00
var userId = User . FindFirstValue ( ClaimTypes . NameIdentifier ) ;
if ( workOrder . Status ! = status )
2026-04-16 18:09:57 -04:00
{
2026-05-14 11:00:12 -05:00
await _workOrderDataService . AddAuditLogAsync ( new WorkOrderAuditLog
2026-04-16 18:09:57 -04:00
{
WorkOrderId = id ,
2026-05-14 11:00:12 -05:00
UserId = userId ,
2026-04-16 18:09:57 -04:00
FieldName = "Status" ,
2026-05-14 11:00:12 -05:00
OldValue = workOrder . Status ? ? "" ,
2026-04-16 18:09:57 -04:00
NewValue = status ,
2026-05-14 11:00:12 -05:00
Action = AuditAction . StatusChange . ToString ( ) ,
2026-04-16 18:09:57 -04:00
CreatedAt = DateTime . UtcNow
} ) ;
}
2026-05-14 11:00:12 -05:00
workOrder . Status = status ;
await _workOrderDataService . UpdateAsync ( workOrder ) ;
return Ok ( new DataResponse { Message = "Updated Successfully" , Status = "200" , Data = WorkOrderDTO . MapToModel ( workOrder ) } ) ;
2024-09-28 14:58:36 -04:00
}
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
{
2026-05-14 11:00:12 -05:00
var workOrder = await _workOrderDataService . GetByIdAsync ( id ) ;
if ( workOrder = = null )
2026-04-16 19:04:59 -04:00
return BadRequest ( new Response { Status = "Error" , Message = "Work order not found" } ) ;
2026-05-14 11:00:12 -05:00
var userId = User . FindFirstValue ( ClaimTypes . NameIdentifier ) ;
if ( workOrder . AssignTo ! = assignTo )
2026-04-16 19:04:59 -04:00
{
2026-05-14 11:00:12 -05:00
var oldUser = workOrder . AssignTo ! = null ? await _userDataService . GetByIdAsync ( workOrder . AssignTo ) : null ;
var newUser = assignTo ! = null ? await _userDataService . GetByIdAsync ( assignTo ) : null ;
await _workOrderDataService . AddAuditLogAsync ( new WorkOrderAuditLog
2026-04-16 19:04:59 -04:00
{
WorkOrderId = id ,
2026-05-14 11:00:12 -05:00
UserId = userId ,
2026-04-16 19:04:59 -04:00
FieldName = "AssignedTo" ,
OldValue = oldUser ! = null ? ( oldUser . FirstName + " " + oldUser . LastName ) . Trim ( ) : "" ,
NewValue = newUser ! = null ? ( newUser . FirstName + " " + newUser . LastName ) . Trim ( ) : "Unassigned" ,
2026-05-14 11:00:12 -05:00
Action = AuditAction . AssignmentChange . ToString ( ) ,
2026-04-16 19:04:59 -04:00
CreatedAt = DateTime . UtcNow
} ) ;
}
2026-05-14 11:00:12 -05:00
workOrder . AssignTo = assignTo ;
await _workOrderDataService . UpdateAsync ( workOrder ) ;
var assignedUser = assignTo ! = null ? await _userDataService . GetByIdAsync ( assignTo ) : null ;
2026-04-16 19:04:59 -04:00
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 ? ? "" ;
2026-05-14 11:00:12 -05:00
var comment = new Comments
{
UserId = userId ,
WorkerOrderId = model . WorkorderId ,
Commenttext = model . Text ,
CommentType = model . CommentType ? ? "vendor"
} ;
2024-09-28 14:58:36 -04:00
2026-05-14 11:00:12 -05:00
if ( model . Document ! = null )
{
var fileUrl = await SaveFileAsync ( model . Document ) ;
comment . Documents = fileUrl ;
2024-09-28 14:58:36 -04:00
}
2026-05-14 11:00:12 -05:00
var saved = await _commentDataService . AddAsync ( comment ) ;
return Ok ( new DataResponse { Message = "Updated Successfully" , Status = "200" , Data = saved } ) ;
2024-09-28 14:58:36 -04:00
}
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
{
UserId = userId ,
WorkerOrderId = model . WorkorderId ,
Commenttext = model . Text ,
CommentType = model . CommentType ? ? "internal" ,
} ;
2026-05-14 11:00:12 -05:00
var saved = await _commentDataService . AddAsync ( comment ) ;
var user = await _userDataService . GetByIdAsync ( userId ) ;
2026-04-16 18:22:34 -04:00
return Ok ( new
{
2026-05-14 11:00:12 -05:00
saved . Id ,
saved . CreatedDate ,
saved . Commenttext ,
saved . CommentType ,
2026-04-16 18:22:34 -04:00
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 ( )
{
2026-05-14 11:00:12 -05:00
var comments = await _commentDataService . GetAllAsync ( ) ;
var data = comments . Select ( s = > new
2024-09-28 14:58:36 -04:00
{
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
2026-05-14 11:00:12 -05:00
} ) . ToList ( ) ;
2024-09-28 14:58:36 -04:00
return Ok ( data ) ;
}
[HttpGet]
[Route("GetCommentsByWorkorderId")]
public async Task < IActionResult > GetCommentsByWorkorderId ( int woid )
{
2026-05-14 11:00:12 -05:00
var comments = await _commentDataService . GetByWorkOrderIdAsync ( woid ) ;
var data = comments . Select ( s = > new
2024-09-28 14:58:36 -04:00
{
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
2026-05-14 11:00:12 -05:00
} ) . ToList ( ) ;
2024-09-28 14:58:36 -04:00
return Ok ( data ) ;
}
2025-02-18 13:13:33 -06:00
[HttpGet]
[Route("GetworkordersDD")]
public async Task < IActionResult > GetworkordersDD ( )
{
2026-05-14 11:00:12 -05:00
var data = await _workOrderDataService . GetAllAsync ( ) ;
return Ok ( data . Where ( w = > w . istemplate ! = true ) ) ;
2025-02-18 13:13:33 -06:00
}
[HttpGet]
[Route("Getworkorders")]
public async Task < IActionResult > Getworkorders ( )
{
2026-05-14 11:00:12 -05:00
var workOrders = await _workOrderDataService . GetAllWithDetailsAsync ( ) ;
var data = workOrders . Select ( s = > new WorkorderFilterVM
{
Id = s . Id ,
Priority = s . Priority ,
Status = s . Status ,
WorkerOrderNumber = s . WorkerOrderNumber ,
WorkerOrderTitle = s . WorkerOrderTitle ,
DueDate = s . DueDate ,
AssignToUserName = s . AssignToUser ! = null ? s . AssignToUser . FirstName : "" ,
LocationName = s . Locations ! = null ? s . Locations . Name : "" ,
LocationId = s . LocationId ,
PO = s . PO ,
TT = s . TT ,
} ) . ToList ( ) ;
2025-02-18 13:13:33 -06:00
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-05-14 11:00:12 -05:00
var allWorkOrders = await _workOrderDataService . GetAllWithDetailsAsync ( ) ;
var workOrders = allWorkOrders . Where ( w = > model . WorkOrderIds . Contains ( w . Id ) ) . ToList ( ) ;
2026-04-17 15:16:28 -04:00
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
2026-05-14 11:00:12 -05:00
var lastPO = await _dispatchDataService . GenerateNextPONumberAsync ( ) ;
int nextPO = int . TryParse ( lastPO , out var po ) ? po : 1 ;
var lastDSP = await _dispatchDataService . GenerateNextDispatchNumberAsync ( ) ;
int nextDSP = lastDSP . StartsWith ( "DSP-" ) & & int . TryParse ( lastDSP . Substring ( 4 ) , out var dspNum ) ? dspNum : 1 ;
2026-04-17 14:01:03 -04:00
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-05-14 11:00:12 -05:00
var currentUser = userId ! = null ? await _userDataService . GetByIdAsync ( userId ) : null ;
2026-04-17 14:01:03 -04:00
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 )
{
2026-05-14 11:00:12 -05:00
var vendor = await _vendorDataService . GetByIdAsync ( 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
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 ,
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:28:52 -04:00
var checklistItems = new List < DispatchChecklistItem > ( ) ;
if ( model . TaskListTemplateId . HasValue )
{
2026-05-14 11:00:12 -05:00
var template = await _taskListTemplateDataService . GetByIdWithItemsAsync ( model . TaskListTemplateId . Value ) ;
2026-04-17 15:28:52 -04:00
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 ;
2026-04-17 15:16:28 -04:00
var woListHtml = string . Join ( "" , workOrders . Select ( wo = >
2026-04-20 12:01:53 -04:00
$"<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>" +
2026-04-17 15:16:28 -04:00
$"<td style='padding:6px 8px;border:1px solid #ddd;'>{wo.DueDate?.ToString(" yyyy - MM - dd ") ?? " N / A "}</td></tr>" ) ) ;
2026-04-20 12:01:53 -04:00
var locationName = System . Net . WebUtility . HtmlEncode ( primaryWO . Locations ? . Name ? ? "" ) ;
2026-04-28 18:55:14 -05:00
var locationAddr = System . Net . WebUtility . HtmlEncode ( primaryWO . Locations ? . Address1 ? ? "" ) ;
2026-04-20 12:01:53 -04:00
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" ;
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-20 12:01:53 -04:00
< p > { descriptionHtml } < / p >
2026-04-17 15:39:50 -04:00
< br / >
< div style = ' text - align : center ; margin : 20 px 0 ; ' >
2026-04-20 12:01:53 -04:00
< a href = ' { portalUrl } ' style = ' display : inline - block ; padding : 12 px 32 px ; background : # 28 a745 ; color : white ; text - decoration : none ; border - radius : 6 px ; font - weight : bold ; font - size : 16 px ; ' > View Work Order < / a >
2026-04-17 15:39:50 -04:00
< / div >
2026-04-20 12:01:53 -04:00
< br / > < p style = ' color : # 666 ; font - size : 13 px ; ' > { senderNameHtml } < 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 ;
2026-05-14 11:00:12 -05:00
var savedDispatch = await _dispatchDataService . AddAsync ( dispatch ) ;
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
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-05-14 11:00:12 -05:00
await _workOrderDataService . AddAuditLogAsync ( new WorkOrderAuditLog
2026-04-17 15:16:28 -04:00
{
WorkOrderId = wo . Id ,
UserId = userId ,
FieldName = "Dispatch" ,
OldValue = "" ,
NewValue = $"{vendor.CompanyName} ({dispatchNumber})" ,
2026-05-14 11:00:12 -05:00
Action = AuditAction . Dispatch . ToString ( ) ,
2026-04-17 15:16:28 -04:00
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-05-14 11:00:12 -05:00
dispatches . Add ( new { savedDispatch . Id , dispatchNumber , poNumber , vendorName = vendor . CompanyName , savedDispatch . 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
}
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-05-14 11:00:12 -05:00
var dispatches = await _dispatchDataService . GetByWorkOrderIdAsync ( workOrderId ) ;
var data = dispatches . OrderByDescending ( d = > d . DispatchedAt ) . Select ( d = > new
{
d . Id ,
d . DispatchNumber ,
d . PONumber ,
d . NTEAmount ,
d . Description ,
d . Status ,
d . ScheduledDate ,
d . CompletedDate ,
d . DispatchedAt ,
d . EmailSent ,
vendorName = d . Vendor ! = null ? d . Vendor . CompanyName : "" ,
vendorEmail = d . Vendor ! = null ? d . Vendor . Email : "" ,
vendorId = d . VendorId
} ) . ToList ( ) ;
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
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 16:03:34 -04:00
x . VerifiedBy ,
x . VerifiedAt ,
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 15:28:52 -04:00
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 ( ) ,
2026-04-17 15:51:20 -04:00
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 ( ) ,
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 ? ? "" ;
2026-04-20 12:01:53 -04:00
var user = ! string . IsNullOrEmpty ( userId ) ? await _db . Users . FindAsync ( userId ) : null ;
var senderName = user ! = null ? ( user . FirstName + " " + user . LastName ) . Trim ( ) : "" ;
2026-04-17 14:01:03 -04:00
var comment = new Comments
{
CreatedDate = DateTime . UtcNow ,
UserId = userId ,
WorkerOrderId = dispatch . WorkOrderId ,
DispatchId = dispatch . Id ,
Commenttext = model . Text ,
2026-04-20 12:01:53 -04:00
CommentType = "dispatcher" ,
Commenter = ! string . IsNullOrWhiteSpace ( senderName ) ? senderName : null ,
2026-04-17 14:01:03 -04:00
} ;
_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 } ) ;
}
}
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-17 16:03:34 -04:00
[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 } ) ;
}
}
2026-04-17 15:51:20 -04:00
[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 } ) ;
}
}
2026-04-17 15:28:52 -04:00
[HttpPost("UpdateChecklistItem")]
public async Task < IActionResult > UpdateChecklistItem ( [ FromBody ] ChecklistItemUpdate_DTO model )
{
2026-05-14 11:00:12 -05:00
var item = await _dispatchDataService . GetChecklistItemByIdAsync ( model . Id ) ;
2026-04-17 15:28:52 -04:00
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 ;
2026-05-14 11:00:12 -05:00
await _dispatchDataService . UpdateChecklistItemAsync ( item ) ;
2026-04-17 15:28:52 -04:00
return Ok ( new { message = "Updated" , item . Id , item . IsCompleted , item . CompletedBy , item . CompletedAt } ) ;
}
[HttpPost("AddChecklistItem")]
public async Task < IActionResult > AddChecklistItem ( [ FromBody ] AddChecklistItem_DTO model )
{
2026-05-14 11:00:12 -05:00
var checklistItems = await _dispatchDataService . GetChecklistItemsAsync ( model . DispatchId ) ;
var maxSort = checklistItems . Any ( ) ? checklistItems . Max ( c = > c . SortOrder ) : - 1 ;
2026-04-17 15:28:52 -04:00
var item = new DispatchChecklistItem
{
DispatchId = model . DispatchId ,
WorkOrderId = model . WorkOrderId ,
ItemText = model . ItemText ,
SortOrder = maxSort + 1 ,
IsCompleted = false
} ;
2026-05-14 11:00:12 -05:00
await _dispatchDataService . AddChecklistItemAsync ( item ) ;
2026-04-17 15:28:52 -04:00
return Ok ( new { item . Id , item . ItemText , item . SortOrder , item . IsCompleted } ) ;
}
2026-04-16 18:42:58 -04:00
private async Task < string > GenerateInternalWONumber ( )
{
2026-05-14 11:00:12 -05:00
return await _workOrderDataService . GenerateNextInternalWONumberAsync ( ) ;
}
private async Task < string > SaveFileAsync ( IFormFile file )
{
var fileName = ContentDispositionHeaderValue . Parse ( file . ContentDisposition ) . FileName . Trim ( '"' ) ;
var uniqueFileName = $"{Guid.NewGuid()}_{fileName}" ;
var uploadPath = Path . Combine ( "Assets" , "Documents" ) ;
var fullPath = Path . Combine ( _webHostEnvironment . WebRootPath , uploadPath , uniqueFileName ) ;
Directory . CreateDirectory ( Path . GetDirectoryName ( fullPath ) ! ) ;
2026-04-16 18:42:58 -04:00
2026-05-14 11:00:12 -05:00
using ( var stream = System . IO . File . Create ( fullPath ) )
{
await file . CopyToAsync ( stream ) ;
}
2026-04-16 18:42:58 -04:00
2026-05-14 11:00:12 -05:00
var request = _httpContext . HttpContext ! . Request ;
var domain = $"{request.Scheme}://{request.Host}" ;
return $"{domain}/{uploadPath.Replace(" \ \ ", " / ")}/{uniqueFileName}" ;
2026-04-16 18:42:58 -04:00
}
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
}
}