mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 09:33:13 +00:00
1562 lines
70 KiB
C#
1562 lines
70 KiB
C#
using Api.SeaHavenIndustries.DTOs;
|
||
using Data.SeaHavenIndustries;
|
||
using Data.SeaHavenIndustries.Enums;
|
||
using FluentValidation;
|
||
using Microsoft.AspNetCore.Authorization;
|
||
using Microsoft.AspNetCore.Components.Forms;
|
||
using Microsoft.AspNetCore.Hosting;
|
||
using Microsoft.AspNetCore.Identity;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||
using Microsoft.Data.SqlClient;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.VisualBasic;
|
||
using SendGrid.Helpers.Mail;
|
||
using System.IO;
|
||
using System.Net.Http.Headers;
|
||
using System.Security.Claims;
|
||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||
using SeaHaven.Services.Interfaces;
|
||
using SeaHaven.Services.DTOs;
|
||
using SeaHaven.Services.Exceptions;
|
||
using SeaHaven.Services.Helpers;
|
||
using SeaHaven.DataServices.Interfaces;
|
||
|
||
namespace Api.SeaHavenIndustries.Controllers
|
||
{
|
||
[Authorize]
|
||
[ApiController]
|
||
[Route("api/WorkOrder")]
|
||
[Route("api/workorders")]
|
||
public class WorkOrderController : Controller
|
||
{
|
||
private readonly UserManager<ApplicationUser> _userManager;
|
||
private readonly IWorkOrderService _workOrderService;
|
||
private readonly IWorkOrderDataService _workOrderDataService;
|
||
private readonly ICommentDataService _commentDataService;
|
||
private readonly IQuotesDataService _quotesDataService;
|
||
private readonly IUserDataService _userDataService;
|
||
private readonly IDispatchDataService _dispatchDataService;
|
||
private readonly IVendorDataService _vendorDataService;
|
||
private readonly ITaskListTemplateDataService _taskListTemplateDataService;
|
||
private readonly ApplicationDbContext _db;
|
||
private readonly IWebHostEnvironment _webHostEnvironment;
|
||
IHttpContextAccessor _httpContext;
|
||
private readonly Helper.SendMessage _sendMessage;
|
||
private readonly Helper.VendorPortalTokenService _vendorTokens;
|
||
private readonly IConfiguration _config;
|
||
private readonly IWorkOrderBoardService _workOrderBoardService;
|
||
private readonly IWorkOrderBoardUpdateService _boardUpdateService;
|
||
private readonly IWorkOrderBoardCreateService _boardCreateService;
|
||
private readonly IWorkOrderBoardCancelService _boardCancelService;
|
||
|
||
public WorkOrderController(
|
||
UserManager<ApplicationUser> userManager,
|
||
IWorkOrderService workOrderService,
|
||
IWorkOrderDataService workOrderDataService,
|
||
ICommentDataService commentDataService,
|
||
IQuotesDataService quotesDataService,
|
||
IUserDataService userDataService,
|
||
IDispatchDataService dispatchDataService,
|
||
IVendorDataService vendorDataService,
|
||
ITaskListTemplateDataService taskListTemplateDataService,
|
||
ApplicationDbContext db,
|
||
IWebHostEnvironment webHostEnvironment,
|
||
IHttpContextAccessor httpContext,
|
||
Helper.SendMessage sendMessage,
|
||
Helper.VendorPortalTokenService vendorTokens,
|
||
IConfiguration config,
|
||
IWorkOrderBoardService workOrderBoardService,
|
||
IWorkOrderBoardUpdateService boardUpdateService,
|
||
IWorkOrderBoardCreateService boardCreateService,
|
||
IWorkOrderBoardCancelService boardCancelService)
|
||
{
|
||
_userManager = userManager;
|
||
_workOrderService = workOrderService;
|
||
_workOrderDataService = workOrderDataService;
|
||
_commentDataService = commentDataService;
|
||
_quotesDataService = quotesDataService;
|
||
_userDataService = userDataService;
|
||
_dispatchDataService = dispatchDataService;
|
||
_vendorDataService = vendorDataService;
|
||
_taskListTemplateDataService = taskListTemplateDataService;
|
||
_db = db;
|
||
_webHostEnvironment = webHostEnvironment;
|
||
_httpContext = httpContext;
|
||
_sendMessage = sendMessage;
|
||
_vendorTokens = vendorTokens;
|
||
_config = config;
|
||
_workOrderBoardService = workOrderBoardService;
|
||
_boardUpdateService = boardUpdateService;
|
||
_boardCreateService = boardCreateService;
|
||
_boardCancelService = boardCancelService;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Weekly board payload.
|
||
/// When <paramref name="myWorkOrders"/> is true, the authenticated user filter wins and
|
||
/// <paramref name="dispatchers"/> is ignored (not AND-combined).
|
||
/// Weekend WOs in a Mon–Sun window may have <c>dayGroup = null</c>; FE should handle that.
|
||
/// </summary>
|
||
[HttpGet("board")]
|
||
public async Task<IActionResult> GetBoard(
|
||
[FromQuery, BindRequired] 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 resolvedWeekEnd = weekEnd ?? weekStart.AddDays(4);
|
||
var weekValidationError = WorkOrderOperationalWeek.ValidateWeekWindow(weekStart, resolvedWeekEnd);
|
||
if (weekValidationError != null)
|
||
return BadRequest(weekValidationError);
|
||
|
||
try
|
||
{
|
||
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);
|
||
}
|
||
catch (ArgumentException ex)
|
||
{
|
||
return BadRequest(ex.Message);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Dispatcher dropdown options. Currently returns all non-deleted users
|
||
/// (role-filtered assignee list deferred until roles settle).
|
||
/// </summary>
|
||
[HttpGet("lookups/dispatchers")]
|
||
public async Task<IActionResult> GetDispatcherLookups()
|
||
{
|
||
var result = await _workOrderBoardService.GetDispatcherLookupsAsync();
|
||
return Ok(result);
|
||
}
|
||
|
||
[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) when (ex.Code == "NotFound")
|
||
{
|
||
return NotFound(new WorkOrderBoardValidationErrorDto
|
||
{
|
||
Code = ex.Code,
|
||
Message = ex.Message
|
||
});
|
||
}
|
||
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 });
|
||
}
|
||
}
|
||
|
||
[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) when (ex.Code == "NotFound")
|
||
{
|
||
return NotFound(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) when (ex.Code == "NotFound")
|
||
{
|
||
return NotFound(new WorkOrderBoardValidationErrorDto
|
||
{
|
||
Code = ex.Code,
|
||
Message = ex.Message
|
||
});
|
||
}
|
||
catch (WorkOrderBoardValidationException ex)
|
||
{
|
||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto
|
||
{
|
||
Code = ex.Code,
|
||
Message = ex.Message
|
||
});
|
||
}
|
||
}
|
||
|
||
[HttpPost]
|
||
[Route("AddWorkorder")]
|
||
public async Task<IActionResult> Addworkorder([FromForm] Workorder_DTO model)
|
||
{
|
||
try
|
||
{
|
||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||
|
||
var createDto = new CreateWorkOrderDTO
|
||
{
|
||
WorkerOrderTitle = model.WorkerOrderTitle ?? "",
|
||
Description = model.Description,
|
||
Status = WorkOrderStatus.Open,
|
||
Priority = model.Priority ?? WorkOrderPriority.Medium,
|
||
LocationId = model.LocationId,
|
||
AssignTo = model.AssignTo,
|
||
DueDate = model.DueDate
|
||
};
|
||
|
||
var workOrderId = await _workOrderService.CreateWorkOrderAsync(createDto, userId!);
|
||
|
||
if (model.ContactIds != null && model.ContactIds.Any())
|
||
{
|
||
await _workOrderDataService.AddWorkOrderContactsAsync(workOrderId, model.ContactIds);
|
||
}
|
||
|
||
if (model.CategoryIds != null && model.CategoryIds.Any())
|
||
{
|
||
await _workOrderDataService.AddWorkOrderCategoriesAsync(workOrderId, model.CategoryIds);
|
||
}
|
||
|
||
if (model.Attachments != null)
|
||
{
|
||
foreach (var attachment in model.Attachments)
|
||
{
|
||
var fileUrl = await SaveFileAsync(attachment);
|
||
await _workOrderDataService.AddWorkOrderAttachmentAsync(new WorkOrderAttachments
|
||
{
|
||
WorkorderId = workOrderId,
|
||
Attachments = fileUrl
|
||
});
|
||
}
|
||
}
|
||
|
||
if (model.BeforPhotoAttachment != null)
|
||
{
|
||
var fileUrl = await SaveFileAsync(model.BeforPhotoAttachment);
|
||
var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId);
|
||
if (workOrder != null)
|
||
{
|
||
workOrder.BeforPhotoAttachment = fileUrl;
|
||
await _workOrderDataService.UpdateAsync(workOrder);
|
||
}
|
||
}
|
||
|
||
if (model.AfterPhotoAttachment != null)
|
||
{
|
||
var fileUrl = await SaveFileAsync(model.AfterPhotoAttachment);
|
||
var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId);
|
||
if (workOrder != null)
|
||
{
|
||
workOrder.AfterPhotoAttachment = fileUrl;
|
||
await _workOrderDataService.UpdateAsync(workOrder);
|
||
}
|
||
}
|
||
|
||
if (model.SignOffAttachment != null)
|
||
{
|
||
var fileUrl = await SaveFileAsync(model.SignOffAttachment);
|
||
var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId);
|
||
if (workOrder != null)
|
||
{
|
||
workOrder.SignOffAttachment = fileUrl;
|
||
await _workOrderDataService.UpdateAsync(workOrder);
|
||
}
|
||
}
|
||
|
||
return Ok(new DataResponse { Message = "Work order created successfully", Status = "200", Data = workOrderId });
|
||
}
|
||
catch (ValidationException vex)
|
||
{
|
||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||
}
|
||
}
|
||
|
||
|
||
[HttpPost]
|
||
[HttpPut("{id:int}")]
|
||
[Route("EditWorkorder")]
|
||
public async Task<IActionResult> Editworkorder([FromForm] EditWorkorder_DTO model)
|
||
{
|
||
try
|
||
{
|
||
if (!model.Id.HasValue)
|
||
return BadRequest(new Response { Status = "Error", Message = "Work order ID is required" });
|
||
|
||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||
|
||
var updateDto = new UpdateWorkOrderDTO
|
||
{
|
||
WorkerOrderTitle = model.WorkerOrderTitle,
|
||
Description = model.Description,
|
||
Status = model.Status,
|
||
Priority = model.Priority,
|
||
LocationId = model.LocationId,
|
||
AssignTo = model.AssignTo,
|
||
DueDate = model.DueDate,
|
||
CompletedDate = model.CompletedDate
|
||
};
|
||
|
||
var success = await _workOrderService.UpdateWorkOrderAsync(model.Id.Value, updateDto, userId!);
|
||
if (!success)
|
||
return BadRequest(new Response { Status = "Error", Message = "Work order not found" });
|
||
|
||
await _workOrderDataService.DeleteWorkOrderContactsAsync(model.Id.Value);
|
||
await _workOrderDataService.DeleteWorkOrderCategoriesAsync(model.Id.Value);
|
||
|
||
if (model.ContactIds != null && model.ContactIds.Any())
|
||
{
|
||
await _workOrderDataService.AddWorkOrderContactsAsync(model.Id.Value, model.ContactIds);
|
||
}
|
||
|
||
if (model.CategoryIds != null && model.CategoryIds.Any())
|
||
{
|
||
await _workOrderDataService.AddWorkOrderCategoriesAsync(model.Id.Value, model.CategoryIds);
|
||
}
|
||
|
||
if (model.Attachments != null)
|
||
{
|
||
foreach (var attachment in model.Attachments)
|
||
{
|
||
var fileUrl = await SaveFileAsync(attachment);
|
||
await _workOrderDataService.AddWorkOrderAttachmentAsync(new WorkOrderAttachments
|
||
{
|
||
WorkorderId = model.Id.Value,
|
||
Attachments = fileUrl
|
||
});
|
||
}
|
||
}
|
||
|
||
if (model.BeforPhotoAttachment != null)
|
||
{
|
||
var fileUrl = await SaveFileAsync(model.BeforPhotoAttachment);
|
||
var workOrder = await _workOrderDataService.GetByIdAsync(model.Id.Value);
|
||
if (workOrder != null)
|
||
{
|
||
workOrder.BeforPhotoAttachment = fileUrl;
|
||
await _workOrderDataService.UpdateAsync(workOrder);
|
||
}
|
||
}
|
||
|
||
if (model.AfterPhotoAttachment != null)
|
||
{
|
||
var fileUrl = await SaveFileAsync(model.AfterPhotoAttachment);
|
||
var workOrder = await _workOrderDataService.GetByIdAsync(model.Id.Value);
|
||
if (workOrder != null)
|
||
{
|
||
workOrder.AfterPhotoAttachment = fileUrl;
|
||
await _workOrderDataService.UpdateAsync(workOrder);
|
||
}
|
||
}
|
||
|
||
if (model.SignOffAttachment != null)
|
||
{
|
||
var fileUrl = await SaveFileAsync(model.SignOffAttachment);
|
||
var workOrder = await _workOrderDataService.GetByIdAsync(model.Id.Value);
|
||
if (workOrder != null)
|
||
{
|
||
workOrder.SignOffAttachment = fileUrl;
|
||
await _workOrderDataService.UpdateAsync(workOrder);
|
||
}
|
||
}
|
||
|
||
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
|
||
}
|
||
catch (ValidationException vex)
|
||
{
|
||
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
||
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||
}
|
||
}
|
||
// -----------------------------------
|
||
// GET: api/WorkOrder/GetWorkOrderList
|
||
// -----------------------------------
|
||
[HttpGet("GetWorkOrderList")]
|
||
public async Task<IActionResult> GetWorkOrderList(string? search = "", int page = 1, int pageSize = 12, string? sortBy = "lastUpdated", string? sortDir = "desc", string? status = "", string? assignee = "", int? locationId = null)
|
||
{
|
||
var (data, totalCount) = await _workOrderDataService.GetWorkOrderListPagedAsync(
|
||
page,
|
||
pageSize,
|
||
search,
|
||
status,
|
||
assignee,
|
||
locationId,
|
||
sortBy,
|
||
sortDir);
|
||
|
||
var viewModel = new Pagination_DTO
|
||
{
|
||
Data = data,
|
||
PageNumber = page,
|
||
PageSize = pageSize,
|
||
TotalCount = totalCount,
|
||
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
|
||
};
|
||
|
||
return Ok(viewModel);
|
||
}
|
||
|
||
|
||
[Authorize(Roles = "Admin")]
|
||
[HttpDelete]
|
||
[Route("DeleteWorkorder")]
|
||
public async Task<IActionResult> Deleteworkorder([FromBody] EditWorkorder_DTO model)
|
||
{
|
||
int id = model.Id.HasValue ? model.Id.Value : 0;
|
||
if (id == 0)
|
||
{
|
||
return BadRequest("Id is required");
|
||
}
|
||
try
|
||
{
|
||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||
var success = await _workOrderService.DeleteWorkOrderAsync(id, userId!);
|
||
|
||
if (!success)
|
||
return BadRequest("Work order not found!");
|
||
|
||
await _quotesDataService.UnlinkFromWorkOrderAsync(id);
|
||
|
||
var comments = await _commentDataService.GetByWorkOrderIdAsync(id);
|
||
foreach (var comment in comments)
|
||
{
|
||
await _commentDataService.DeleteAsync(comment.Id);
|
||
}
|
||
|
||
return Ok(new DataResponse { Message = "Removed Successfully", Status = "200" });
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||
}
|
||
}
|
||
|
||
[HttpGet]
|
||
[Route("GetFilteredWorkorder")]
|
||
public IActionResult GetFilteredWorkorder(
|
||
[FromQuery] string[] assignto,
|
||
[FromQuery] int[]? location,
|
||
[FromQuery] string[]? priority,
|
||
[FromQuery] string[]? status,
|
||
[FromQuery] string[]? duedate,
|
||
[FromQuery] string search = "",
|
||
[FromQuery] string sort = "Sort Order",
|
||
[FromQuery] string sortby = "Sort By",
|
||
int page = 1,
|
||
int pageSize = 10)
|
||
{
|
||
try
|
||
{
|
||
IQueryable<WorkOrder> query = _db.workOrders.Where(w => w.istemplate != true).Include(w => w.Locations);
|
||
|
||
if (!string.IsNullOrEmpty(search))
|
||
{
|
||
var s = search.ToLower();
|
||
query = query.Where(w =>
|
||
(w.WorkerOrderTitle ?? "").ToLower().Contains(s) ||
|
||
(w.WorkerOrderNumber ?? "").ToLower().Contains(s) ||
|
||
(w.Locations != null && (w.Locations.Title ?? "").ToLower().Contains(s)));
|
||
}
|
||
|
||
if (assignto != null && assignto.Length > 0)
|
||
{
|
||
if (assignto[0] != "" && assignto[0] != null)
|
||
query = query.Where(w => assignto.Contains(w.AssignTo));
|
||
}
|
||
|
||
if (location != null && location.Length > 0)
|
||
{
|
||
if (location[0] != 0)
|
||
query = query.Where(w => location.Contains(w.LocationId ?? 0));
|
||
}
|
||
|
||
if (priority != null && priority.Length > 0)
|
||
{
|
||
if (priority[0] != "" && priority[0] != null)
|
||
query = query.Where(w => priority.Contains(w.Priority));
|
||
}
|
||
|
||
if (status != null && status.Length > 0)
|
||
{
|
||
if (status[0] != "" && status[0] != null)
|
||
query = query.Where(w => status.Contains(w.Status));
|
||
}
|
||
var data = query.ToList();
|
||
|
||
// Perform client-side filtering for due dates
|
||
data = data.Where(w =>
|
||
duedate == null || duedate.Length == 0 || duedate[0] == null ||
|
||
duedate.Any(dd => w.DueDate.HasValue && CheckDueDate(dd, w.DueDate.Value.Date, DateTime.UtcNow.Date))).ToList();
|
||
|
||
// Apply sorting
|
||
if (!string.IsNullOrEmpty(sort) && !string.IsNullOrEmpty(sortby) && sortby != "")
|
||
{
|
||
switch (sortby)
|
||
{
|
||
case "Name":
|
||
data = (sort.ToLower() == "ascending") ? data.OrderBy(w => w.WorkerOrderTitle).ToList() : data.OrderByDescending(w => w.WorkerOrderTitle).ToList();
|
||
break;
|
||
case "Due Date":
|
||
data = (sort.ToLower() == "ascending") ? data.OrderBy(w => w.DueDate).ToList() : data.OrderByDescending(w => w.DueDate).ToList();
|
||
break;
|
||
case "Severity":
|
||
data = (sort.ToLower() == "ascending") ? data.OrderBy(w => w.Priority).ToList() : data.OrderByDescending(w => w.Priority).ToList();
|
||
break;
|
||
default:
|
||
// Handle default case or throw an exception
|
||
break;
|
||
}
|
||
}
|
||
|
||
var totalCount = data.Count();
|
||
var pagedData = data
|
||
.Skip((page - 1) * pageSize)
|
||
.Take(pageSize)
|
||
.Select(s => new { s.Id, s.WorkerOrderTitle, s.DueDate, s.Status, s.Priority, Name = s.Locations != null ? s.Locations.Name : "" })
|
||
.OrderByDescending(d => d.Name)
|
||
.ToList();
|
||
|
||
var totalPages1 = (int)Math.Ceiling(totalCount / (double)pageSize);
|
||
|
||
// Create a view model containing the paged items and pagination metadata
|
||
var viewModel = new Pagination_DTO
|
||
{
|
||
Data = pagedData,
|
||
PageNumber = page,
|
||
PageSize = pageSize,
|
||
TotalCount = totalCount,
|
||
TotalPages = totalPages1
|
||
};
|
||
|
||
return Ok(viewModel);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// Handle exception and return a default result or an error response
|
||
return StatusCode(500, "Internal Server Error");
|
||
}
|
||
}
|
||
|
||
[HttpGet("{id:int}")]
|
||
[HttpGet("GetWorkorderById")]
|
||
public async Task<IActionResult> GetWorkorderById(int id)
|
||
{
|
||
var exist = await _db.workOrders.Include(w => w.Locations).Include(w => w.Comments).Include(w => w.Quotes).Include(w => w.Dispatches).ThenInclude(d => d.Vendor).Where(w => w.Id == id).Select(s => new
|
||
{
|
||
s.Id,
|
||
s.InternalWONumber,
|
||
s.WorkerOrderNumber,
|
||
title = s.WorkerOrderTitle,
|
||
s.Description,
|
||
s.CreatedDate,
|
||
s.DueDate,
|
||
s.ScheduledDate,
|
||
s.CompletedDate,
|
||
s.Status,
|
||
assignee = s.AssignToUser != null ? s.AssignToUser.FirstName : "",
|
||
location = s.Locations != null ? s.Locations.Name : "",
|
||
locationId = s.LocationId,
|
||
locationZip = s.Locations != null ? (s.Locations.Zip ?? ExtractZip(s.Locations.Address1)) : "",
|
||
priority = s.Priority,
|
||
s.Problem,
|
||
s.Trade,
|
||
s.SubTrade,
|
||
s.VendorNTE,
|
||
s.Source,
|
||
s.PO,
|
||
s.TT,
|
||
poc = s.WorkOrderContacts.Select(c => new { c.Id, name = c.POC != null ? (c.POC.FirstName + " " + c.POC.LastName).Trim() : "", email = c.POC != null ? c.POC.Email : "", phone = c.POC != null ? c.POC.PhoneNumber : "" }),
|
||
category = s.WorkOrderCategories.Select(c => new { c.Id, name = c.Category != null ? c.Category.Name : "" }),
|
||
Attachments = s.workOrderAttachments.Select(a => new { a.Id, attachment = a.Attachments }),
|
||
s.BeforPhotoIssue,
|
||
s.BeforPhotoAttachment,
|
||
s.AfterPhotoIssue,
|
||
s.AfterPhotoAttachment,
|
||
s.SignOffName,
|
||
s.SignOffAttachment,
|
||
s.SignOffSignature,
|
||
Comments = s.Comments.Select(c => new { c.Id, c.CreatedDate, c.Commenttext, c.Commenter, c.RecordType, c.CommentType, c.Documents, userName = c.ApplicationUser != null ? c.ApplicationUser.FirstName : "" }),
|
||
quotes = s.Quotes.Select(q => new { code = q.QuotId, q.Id }),
|
||
auditLog = _db.WorkOrderAuditLogs
|
||
.Where(a => a.WorkOrderId == s.Id)
|
||
.OrderByDescending(a => a.CreatedAt)
|
||
.Select(a => new {
|
||
a.Id,
|
||
a.FieldName,
|
||
a.OldValue,
|
||
a.NewValue,
|
||
a.Action,
|
||
a.CreatedAt,
|
||
userName = a.User != null ? a.User.FirstName + " " + a.User.LastName : ""
|
||
}).ToList(),
|
||
dispatches = s.Dispatches.Select(d => new {
|
||
d.Id,
|
||
d.DispatchNumber,
|
||
d.PONumber,
|
||
d.NTEAmount,
|
||
d.Description,
|
||
d.Status,
|
||
d.ScheduledDate,
|
||
d.EstimatedArrivalAt,
|
||
d.EtaManualOverride,
|
||
d.CompletedDate,
|
||
d.DispatchedAt,
|
||
d.EmailSent,
|
||
vendorName = d.Vendor != null ? d.Vendor.CompanyName : "",
|
||
vendorEmail = d.Vendor != null ? d.Vendor.Email : "",
|
||
vendorId = d.VendorId
|
||
}).ToList()
|
||
}).FirstOrDefaultAsync();
|
||
if (exist == null)
|
||
{
|
||
|
||
return BadRequest(new Response { Status = "Error", Message = "ID not found!" });
|
||
}
|
||
else
|
||
{
|
||
return Ok(exist);
|
||
}
|
||
}
|
||
[HttpGet]
|
||
[Route("GetFilteredWorkorder2")]
|
||
public IActionResult GetFilteredWorkorder2([FromQuery] string[] assignto, [FromQuery] int[]? location, [FromQuery] string[]? priority, [FromQuery] string[]? status, [FromQuery] string[]? duedate, [FromQuery] string search = "", [FromQuery] string sort = "Sort Order", [FromQuery] string sortby = "Sort By", int page = 1, int pageSize = 10)
|
||
{
|
||
try
|
||
{
|
||
var totalCount = _db.workOrders.Where(w => w.istemplate != true).Count();
|
||
if (sort == "Sort Order" && sortby == "Sort By" && assignto.Count() == 0 && (duedate == null || duedate.Count() == 0) && location.Count() == 0 && (priority == null || priority.Count() == 0) && (status == null || status.Count() == 0) && search == "")
|
||
{
|
||
var data1 = _db.workOrders.Where(w => w.istemplate != true).Include(w => w.Locations).ToListAsync();
|
||
return Ok(data1);
|
||
}
|
||
//if (sort == "Sort Order" && sortby == "Sort By" && assignto.Count() == 0 && (duedate == null || duedate.Count() == 0) && location.Count() == 0 && (priority == null || priority.Count() == 0) && (status == null || status.Count() == 0) && search == "")
|
||
//{
|
||
// var data1 = _db.workOrders.Where(w => w.istemplate != true).Include(w => w.Locations).Select(s => new {s.Id, s.WorkerOrderTitle, s.DueDate, s.Status, s.Priority, s.Locations.Name }).OrderByDescending(d => d.Name).ToList().Skip((page - 1) * pageSize)
|
||
// .Take(pageSize);
|
||
|
||
// // Calculate the number of pages
|
||
// var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
||
|
||
// // Create a view model containing the paged items and pagination metadata
|
||
// var viewModel1 = new Pagination_DTO
|
||
// {
|
||
// Data = data1,
|
||
// PageNumber = page,
|
||
// PageSize = pageSize,
|
||
// TotalCount = totalCount,
|
||
// TotalPages = totalPages
|
||
// };
|
||
// return Ok(viewModel1);
|
||
//}
|
||
|
||
var currentDate = DateTime.UtcNow.Date;
|
||
var data = _db.workOrders.Include(w => w.Locations)
|
||
.Where(w => w.istemplate != true && (((w.WorkerOrderTitle ?? "").ToLower().Contains(search.ToLower()) || (w.WorkerOrderNumber ?? "").ToLower().Contains(search.ToLower()) || (w.Locations != null && (w.Locations.Title ?? "").ToLower().Contains(search.ToLower()))) &&
|
||
(assignto.Count() == 0 || assignto.Contains(w.AssignTo)) &&
|
||
(location.Count() == 0 || (w.LocationId.HasValue && location.Contains(w.LocationId.Value))) &&
|
||
(priority.Count() == 0 || priority.Contains(w.Priority)) &&
|
||
((status.Count() == 0 || status.Contains(w.Status))
|
||
)))
|
||
.ToList();
|
||
|
||
// Perform client-side filtering for due dates
|
||
data = data.Where(w =>
|
||
duedate.Count() == 0 ||
|
||
duedate.Any(dd => w.DueDate.HasValue && CheckDueDate(dd, w.DueDate.Value.Date, currentDate))).ToList();
|
||
|
||
// Apply sorting
|
||
if (!string.IsNullOrEmpty(sort) && !string.IsNullOrEmpty(sortby) && sortby != "")
|
||
{
|
||
switch (sortby)
|
||
{
|
||
case "Name":
|
||
data = (sort.ToLower() == "ascending") ? data.OrderBy(w => w.WorkerOrderTitle).ToList() : data.OrderByDescending(w => w.WorkerOrderTitle).ToList();
|
||
break;
|
||
case "Due Date":
|
||
data = (sort.ToLower() == "ascending") ? data.OrderBy(w => w.DueDate).ToList() : data.OrderByDescending(w => w.DueDate).ToList();
|
||
break;
|
||
case "Severity":
|
||
data = (sort.ToLower() == "ascending") ? data.OrderBy(w => w.Priority).ToList() : data.OrderByDescending(w => w.Priority).ToList();
|
||
break;
|
||
default:
|
||
// Handle default case or throw an exception
|
||
break;
|
||
}
|
||
}
|
||
var newdata = data.Select(s => new { s.Id, s.WorkerOrderTitle, s.DueDate, s.Status, s.Priority, Name = s.Locations != null ? s.Locations.Name : "" }).OrderByDescending(d => d.Name).ToList().Skip((page - 1) * pageSize)
|
||
.Take(pageSize);
|
||
var totalPages1 = (int)Math.Ceiling(totalCount / (double)pageSize);
|
||
|
||
// Create a view model containing the paged items and pagination metadata
|
||
var viewModel = new Pagination_DTO
|
||
{
|
||
Data = newdata,
|
||
PageNumber = page,
|
||
PageSize = pageSize,
|
||
TotalCount = totalCount,
|
||
TotalPages = totalPages1
|
||
};
|
||
return Ok(viewModel);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var totalCount = _db.workOrders.Where(w => w.istemplate != true).Count();
|
||
var data1 = _db.workOrders.Where(w => w.istemplate != true).Include(w => w.Locations).Select(s => new
|
||
{
|
||
s.Id,
|
||
s.WorkerOrderTitle,
|
||
s.DueDate,
|
||
s.Status,
|
||
s.Priority,
|
||
Name = s.Locations != null ? s.Locations.Name : ""
|
||
}).OrderByDescending(d => d.Id).ToList().Skip((page - 1) * pageSize)
|
||
.Take(pageSize);
|
||
|
||
// Calculate the number of pages
|
||
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
||
|
||
// Create a view model containing the paged items and pagination metadata
|
||
var viewModel = new Pagination_DTO
|
||
{
|
||
Data = data1,
|
||
PageNumber = page,
|
||
PageSize = pageSize,
|
||
TotalCount = totalCount,
|
||
TotalPages = totalPages
|
||
};
|
||
return Ok(viewModel);
|
||
}
|
||
}
|
||
bool CheckDueDate(string dd, DateTime dueDate, DateTime currentDate)
|
||
{
|
||
return dd switch
|
||
{
|
||
"Today" => dueDate == currentDate,
|
||
"Tomorrow" => dueDate == currentDate.AddDays(1),
|
||
"Next Week" => dueDate >= currentDate.AddDays(1) && dueDate <= currentDate.AddDays(7),
|
||
"Next Month" => dueDate >= currentDate.AddDays(1) && dueDate <= currentDate.AddMonths(1),
|
||
"Over Due" => dueDate < currentDate,
|
||
_ => false
|
||
};
|
||
}
|
||
|
||
[HttpPost]
|
||
[Route("ChangeStatus")]
|
||
public async Task<IActionResult> ChangeStatus(int id, string status)
|
||
{
|
||
try
|
||
{
|
||
var workOrder = await _workOrderDataService.GetByIdAsync(id);
|
||
if (workOrder == null)
|
||
return BadRequest(new Response { Status = "Error", Message = "Work order not found" });
|
||
|
||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||
|
||
if (workOrder.Status != status)
|
||
{
|
||
await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog
|
||
{
|
||
WorkOrderId = id,
|
||
UserId = userId,
|
||
FieldName = "Status",
|
||
OldValue = workOrder.Status ?? "",
|
||
NewValue = status,
|
||
Action = AuditAction.StatusChange.ToString(),
|
||
CreatedAt = DateTime.UtcNow
|
||
});
|
||
}
|
||
|
||
workOrder.Status = status;
|
||
await _workOrderDataService.UpdateAsync(workOrder);
|
||
|
||
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200", Data = WorkOrderDTO.MapToModel(workOrder) });
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||
}
|
||
}
|
||
|
||
[HttpPost]
|
||
[Route("ChangeAssignment")]
|
||
public async Task<IActionResult> ChangeAssignment(int id, string? assignTo)
|
||
{
|
||
try
|
||
{
|
||
var workOrder = await _workOrderDataService.GetByIdAsync(id);
|
||
if (workOrder == null)
|
||
return BadRequest(new Response { Status = "Error", Message = "Work order not found" });
|
||
|
||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||
|
||
if (workOrder.AssignTo != assignTo)
|
||
{
|
||
var oldUser = workOrder.AssignTo != null ? await _userDataService.GetByIdAsync(workOrder.AssignTo) : null;
|
||
var newUser = assignTo != null ? await _userDataService.GetByIdAsync(assignTo) : null;
|
||
|
||
await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog
|
||
{
|
||
WorkOrderId = id,
|
||
UserId = userId,
|
||
FieldName = "AssignedTo",
|
||
OldValue = oldUser != null ? (oldUser.FirstName + " " + oldUser.LastName).Trim() : "",
|
||
NewValue = newUser != null ? (newUser.FirstName + " " + newUser.LastName).Trim() : "Unassigned",
|
||
Action = AuditAction.AssignmentChange.ToString(),
|
||
CreatedAt = DateTime.UtcNow
|
||
});
|
||
}
|
||
|
||
workOrder.AssignTo = assignTo;
|
||
await _workOrderDataService.UpdateAsync(workOrder);
|
||
|
||
var assignedUser = assignTo != null ? await _userDataService.GetByIdAsync(assignTo) : null;
|
||
var assignedName = assignedUser != null ? (assignedUser.FirstName + " " + assignedUser.LastName).Trim() : "";
|
||
return Ok(new { message = "Updated Successfully", assignedTo = assignedName });
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||
}
|
||
}
|
||
|
||
[HttpPost]
|
||
[Route("AddComment")]
|
||
public async Task<IActionResult> AddComment([FromForm] Comments_DTO model)
|
||
{
|
||
try
|
||
{
|
||
var userId = _httpContext?.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "";
|
||
|
||
var comment = new Comments
|
||
{
|
||
UserId = userId,
|
||
WorkerOrderId = model.WorkorderId,
|
||
Commenttext = model.Text,
|
||
CommentType = model.CommentType ?? "vendor"
|
||
};
|
||
|
||
if (model.Document != null)
|
||
{
|
||
var fileUrl = await SaveFileAsync(model.Document);
|
||
comment.Documents = fileUrl;
|
||
}
|
||
|
||
var saved = await _commentDataService.AddAsync(comment);
|
||
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200", Data = saved });
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||
}
|
||
}
|
||
|
||
[HttpPost]
|
||
[Route("AddCommentJson")]
|
||
public async Task<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",
|
||
};
|
||
|
||
var saved = await _commentDataService.AddAsync(comment);
|
||
var user = await _userDataService.GetByIdAsync(userId);
|
||
|
||
return Ok(new
|
||
{
|
||
saved.Id,
|
||
saved.CreatedDate,
|
||
saved.Commenttext,
|
||
saved.CommentType,
|
||
userName = user != null ? (user.FirstName + " " + user.LastName).Trim() : ""
|
||
});
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||
}
|
||
}
|
||
|
||
[HttpGet]
|
||
[Route("GetComments")]
|
||
public async Task<IActionResult> GetComments()
|
||
{
|
||
var comments = await _commentDataService.GetAllAsync();
|
||
var data = comments.Select(s => new
|
||
{
|
||
s.Id,
|
||
s.Commenttext,
|
||
FirstName = s.ApplicationUser != null ? s.ApplicationUser.FirstName : "",
|
||
s.Documents
|
||
}).ToList();
|
||
return Ok(data);
|
||
}
|
||
|
||
[HttpGet]
|
||
[Route("GetCommentsByWorkorderId")]
|
||
public async Task<IActionResult> GetCommentsByWorkorderId(int woid)
|
||
{
|
||
var comments = await _commentDataService.GetByWorkOrderIdAsync(woid);
|
||
var data = comments.Select(s => new
|
||
{
|
||
s.Id,
|
||
s.Commenttext,
|
||
FirstName = s.ApplicationUser != null ? s.ApplicationUser.FirstName : "",
|
||
s.Documents
|
||
}).ToList();
|
||
return Ok(data);
|
||
}
|
||
[HttpGet]
|
||
[Route("GetworkordersDD")]
|
||
public async Task<IActionResult> GetworkordersDD()
|
||
{
|
||
var data = await _workOrderDataService.GetAllAsync();
|
||
return Ok(data.Where(w => w.istemplate != true));
|
||
}
|
||
[HttpGet]
|
||
[Route("Getworkorders")]
|
||
public async Task<IActionResult> Getworkorders()
|
||
{
|
||
var workOrders = await _workOrderDataService.GetAllWithDetailsAsync();
|
||
var data = workOrders.Select(s => new WorkorderFilterVM
|
||
{
|
||
Id = s.Id,
|
||
Priority = s.Priority,
|
||
Status = s.Status,
|
||
WorkerOrderNumber = s.WorkerOrderNumber,
|
||
WorkerOrderTitle = s.WorkerOrderTitle,
|
||
DueDate = s.DueDate,
|
||
AssignToUserName = s.AssignToUser != null ? s.AssignToUser.FirstName : "",
|
||
LocationName = s.Locations != null ? s.Locations.Name : "",
|
||
LocationId = s.LocationId,
|
||
PO = s.PO,
|
||
TT = s.TT,
|
||
}).ToList();
|
||
return Ok(data);
|
||
}
|
||
[HttpPost]
|
||
[Route("DispatchToVendor")]
|
||
public async Task<IActionResult> DispatchToVendor([FromBody] Dispatch_DTO model)
|
||
{
|
||
try
|
||
{
|
||
var allWorkOrders = await _workOrderDataService.GetAllWithDetailsAsync();
|
||
var workOrders = allWorkOrders.Where(w => model.WorkOrderIds.Contains(w.Id)).ToList();
|
||
|
||
if (workOrders.Count == 0)
|
||
return BadRequest(new Response { Status = "Error", Message = "No work orders found" });
|
||
|
||
var primaryWO = workOrders.First();
|
||
|
||
var lastPO = await _dispatchDataService.GenerateNextPONumberAsync();
|
||
int nextPO = int.TryParse(lastPO, out var po) ? po : 1;
|
||
|
||
var lastDSP = await _dispatchDataService.GenerateNextDispatchNumberAsync();
|
||
int nextDSP = lastDSP.StartsWith("DSP-") && int.TryParse(lastDSP.Substring(4), out var dspNum) ? dspNum : 1;
|
||
|
||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||
var currentUser = userId != null ? await _userDataService.GetByIdAsync(userId) : null;
|
||
var senderName = currentUser != null ? (currentUser.FirstName + " " + currentUser.LastName).Trim() : "";
|
||
var dispatches = new List<object>();
|
||
|
||
foreach (var vendorId in model.VendorIds)
|
||
{
|
||
var vendor = await _vendorDataService.GetByIdAsync(vendorId);
|
||
if (vendor == null)
|
||
return BadRequest(new Response { Status = "Error", Message = $"Vendor {vendorId} was not found" });
|
||
if (!vendor.IsActive || (vendor.IsDeleted ?? false))
|
||
return BadRequest(new Response { Status = "Error", Message = $"Vendor {vendorId} is inactive and cannot receive a new assignment" });
|
||
|
||
var poNumber = $"VPO-{nextPO:D5}";
|
||
nextPO++;
|
||
var dispatchNumber = $"DSP-{nextDSP:D5}";
|
||
nextDSP++;
|
||
|
||
var replyTo = $"wo-{primaryWO.InternalWONumber}-{dispatchNumber}@int.seahaven.com";
|
||
|
||
var dispatch = new Dispatch
|
||
{
|
||
WorkOrderId = primaryWO.Id,
|
||
VendorId = vendorId,
|
||
DispatchNumber = dispatchNumber,
|
||
PONumber = poNumber,
|
||
NTEAmount = model.NTEAmount,
|
||
Description = model.Description,
|
||
Status = "Sent",
|
||
ScheduledDate = model.ScheduledDate,
|
||
EstimatedArrivalAt = model.EstimatedArrivalAt,
|
||
EtaManualOverride = model.EtaManualOverride,
|
||
DispatchedAt = DateTime.UtcNow,
|
||
ReplyToAddress = replyTo,
|
||
DispatchWorkOrders = workOrders.Select(wo => new DispatchWorkOrder { WorkOrderId = wo.Id }).ToList()
|
||
};
|
||
|
||
var checklistItems = new List<DispatchChecklistItem>();
|
||
if (model.TaskListTemplateId.HasValue)
|
||
{
|
||
var template = await _taskListTemplateDataService.GetByIdWithItemsAsync(model.TaskListTemplateId.Value);
|
||
if (template?.Items != null)
|
||
{
|
||
checklistItems.AddRange(template.Items.OrderBy(i => i.SortOrder).Select((i, idx) =>
|
||
new DispatchChecklistItem { ItemText = i.ItemText, SortOrder = idx }));
|
||
}
|
||
}
|
||
if (model.CustomChecklistItems != null)
|
||
{
|
||
var startIdx = checklistItems.Count;
|
||
checklistItems.AddRange(model.CustomChecklistItems.Select((text, idx) =>
|
||
new DispatchChecklistItem { ItemText = text, SortOrder = startIdx + idx }));
|
||
}
|
||
dispatch.ChecklistItems = checklistItems;
|
||
|
||
var woListHtml = string.Join("", workOrders.Select(wo =>
|
||
$"<tr><td style='padding:6px 8px;border:1px solid #ddd;'>{System.Net.WebUtility.HtmlEncode(wo.InternalWONumber ?? "")}</td>" +
|
||
$"<td style='padding:6px 8px;border:1px solid #ddd;'>{System.Net.WebUtility.HtmlEncode(wo.WorkerOrderTitle ?? "")}</td>" +
|
||
$"<td style='padding:6px 8px;border:1px solid #ddd;'>{System.Net.WebUtility.HtmlEncode(wo.Priority ?? "")}</td>" +
|
||
$"<td style='padding:6px 8px;border:1px solid #ddd;'>{wo.DueDate?.ToString("yyyy-MM-dd") ?? "N/A"}</td></tr>"));
|
||
|
||
var locationName = System.Net.WebUtility.HtmlEncode(primaryWO.Locations?.Name ?? "");
|
||
var locationAddr = System.Net.WebUtility.HtmlEncode(primaryWO.Locations?.Address1 ?? "");
|
||
var subject = $"[{dispatchNumber}] {(workOrders.Count > 1 ? $"{workOrders.Count} Work Orders" : primaryWO.WorkerOrderTitle)} at {primaryWO.Locations?.Name ?? ""}";
|
||
var descriptionHtml = System.Net.WebUtility.HtmlEncode(model.Description ?? primaryWO.Description ?? "");
|
||
var senderNameHtml = System.Net.WebUtility.HtmlEncode(senderName);
|
||
|
||
var vendorToken = await _vendorTokens.GetOrCreateActiveTokenAsync(vendorId);
|
||
var frontendBase = _config.GetValue<string>("FrontendBaseUrl")?.TrimEnd('/') ?? "";
|
||
var portalUrl = $"{frontendBase}/v/{vendorToken.Token}/dashboard";
|
||
|
||
var html = $@"
|
||
<h2>Work Order Dispatch — {dispatchNumber}</h2>
|
||
<p><strong>Location:</strong> {locationName} {locationAddr}</p>
|
||
<p><strong>Vendor PO:</strong> {poNumber} <strong>NTE:</strong> ${model.NTEAmount?.ToString("F2") ?? "N/A"}</p>
|
||
<h3>Work Orders</h3>
|
||
<table style='border-collapse:collapse;width:100%;font-family:Arial,sans-serif;'>
|
||
<tr style='background:#f5f5f5;'>
|
||
<th style='padding:6px 8px;border:1px solid #ddd;text-align:left;'>WO #</th>
|
||
<th style='padding:6px 8px;border:1px solid #ddd;text-align:left;'>Title</th>
|
||
<th style='padding:6px 8px;border:1px solid #ddd;text-align:left;'>Priority</th>
|
||
<th style='padding:6px 8px;border:1px solid #ddd;text-align:left;'>Due Date</th>
|
||
</tr>
|
||
{woListHtml}
|
||
</table>
|
||
<h3>Description</h3>
|
||
<p>{descriptionHtml}</p>
|
||
<br/>
|
||
<div style='text-align:center;margin:20px 0;'>
|
||
<a href='{portalUrl}' style='display:inline-block;padding:12px 32px;background:#28a745;color:white;text-decoration:none;border-radius:6px;font-weight:bold;font-size:16px;'>View Work Order</a>
|
||
</div>
|
||
<br/><p style='color:#666;font-size:13px;'>{senderNameHtml}<br/>Sea Haven Industries</p>
|
||
<hr/><p style='color:#999;font-size:12px;'>Reply to this email to communicate about this dispatch.</p>";
|
||
|
||
var emailSent = await _sendMessage.SendDispatchEmail(vendor.Email, subject, html, replyTo);
|
||
dispatch.EmailSent = emailSent;
|
||
|
||
var savedDispatch = await _dispatchDataService.AddAsync(dispatch);
|
||
|
||
foreach (var wo in workOrders)
|
||
{
|
||
await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog
|
||
{
|
||
WorkOrderId = wo.Id,
|
||
UserId = userId,
|
||
FieldName = "Dispatch",
|
||
OldValue = "",
|
||
NewValue = $"{vendor.CompanyName} ({dispatchNumber})",
|
||
Action = AuditAction.Dispatch.ToString(),
|
||
CreatedAt = DateTime.UtcNow
|
||
});
|
||
if (model.EstimatedArrivalAt.HasValue)
|
||
{
|
||
await _workOrderDataService.AddAuditLogAsync(new WorkOrderAuditLog
|
||
{
|
||
WorkOrderId = wo.Id,
|
||
UserId = userId,
|
||
FieldName = "Vendor ETA",
|
||
OldValue = "",
|
||
NewValue = model.EstimatedArrivalAt.Value.ToUniversalTime().ToString("O"),
|
||
Action = model.EtaManualOverride ? "vendor_eta_manual_override" : "vendor_eta_set",
|
||
CreatedAt = DateTime.UtcNow
|
||
});
|
||
}
|
||
}
|
||
|
||
dispatches.Add(new { savedDispatch.Id, dispatchNumber, poNumber, vendorName = vendor.CompanyName, savedDispatch.Status, workOrderCount = workOrders.Count });
|
||
}
|
||
|
||
return Ok(new { message = "Dispatched", dispatches });
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||
}
|
||
}
|
||
|
||
[HttpGet("GetDispatches/{workOrderId}")]
|
||
public async Task<IActionResult> GetDispatches(int workOrderId)
|
||
{
|
||
var dispatches = await _dispatchDataService.GetByWorkOrderIdAsync(workOrderId);
|
||
var data = dispatches.OrderByDescending(d => d.DispatchedAt).Select(d => new
|
||
{
|
||
d.Id,
|
||
d.DispatchNumber,
|
||
d.PONumber,
|
||
d.NTEAmount,
|
||
d.Description,
|
||
d.Status,
|
||
d.ScheduledDate,
|
||
d.EstimatedArrivalAt,
|
||
d.EtaManualOverride,
|
||
d.CompletedDate,
|
||
d.DispatchedAt,
|
||
d.EmailSent,
|
||
vendorName = d.Vendor != null ? d.Vendor.CompanyName : "",
|
||
vendorEmail = d.Vendor != null ? d.Vendor.Email : "",
|
||
vendorId = d.VendorId
|
||
}).ToList();
|
||
return Ok(data);
|
||
}
|
||
|
||
[HttpGet("GetDispatch/{id}")]
|
||
public async Task<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.EstimatedArrivalAt,
|
||
x.EtaManualOverride,
|
||
x.CompletedDate,
|
||
x.DispatchedAt,
|
||
x.EmailSent,
|
||
x.WorkOrderId,
|
||
x.AcknowledgedAt,
|
||
x.VerifiedBy,
|
||
x.VerifiedAt,
|
||
woNumber = x.WorkOrder != null ? x.WorkOrder.InternalWONumber : "",
|
||
woTitle = x.WorkOrder != null ? x.WorkOrder.WorkerOrderTitle : "",
|
||
vendorName = x.Vendor != null ? x.Vendor.CompanyName : "",
|
||
vendorEmail = x.Vendor != null ? x.Vendor.Email : "",
|
||
vendorId = x.VendorId,
|
||
workOrders = _db.DispatchWorkOrders
|
||
.Where(dwo => dwo.DispatchId == x.Id)
|
||
.Select(dwo => new {
|
||
dwo.WorkOrderId,
|
||
woNumber = dwo.WorkOrder != null ? dwo.WorkOrder.InternalWONumber : "",
|
||
woTitle = dwo.WorkOrder != null ? dwo.WorkOrder.WorkerOrderTitle : ""
|
||
}).ToList(),
|
||
checklistItems = _db.DispatchChecklistItems
|
||
.Where(ci => ci.DispatchId == x.Id)
|
||
.OrderBy(ci => ci.SortOrder)
|
||
.Select(ci => new {
|
||
ci.Id,
|
||
ci.ItemText,
|
||
ci.IsCompleted,
|
||
ci.CompletedBy,
|
||
ci.CompletedAt,
|
||
ci.SortOrder,
|
||
ci.WorkOrderId
|
||
}).ToList(),
|
||
signoffs = _db.DispatchSignoffs
|
||
.Where(s => s.DispatchId == x.Id)
|
||
.Select(s => new {
|
||
s.Id,
|
||
s.SignoffType,
|
||
s.Name,
|
||
s.Signature,
|
||
s.SignatureMethod,
|
||
s.SignedAt
|
||
}).ToList(),
|
||
comments = _db.Comments
|
||
.Where(c => c.DispatchId == x.Id)
|
||
.OrderBy(c => c.CreatedDate)
|
||
.Select(c => new
|
||
{
|
||
c.Id,
|
||
c.CreatedDate,
|
||
c.Commenttext,
|
||
c.Commenter,
|
||
c.CommentType,
|
||
userName = c.ApplicationUser != null ? c.ApplicationUser.FirstName + " " + c.ApplicationUser.LastName : ""
|
||
}).ToList()
|
||
}).FirstOrDefaultAsync();
|
||
|
||
if (d == null)
|
||
return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
|
||
return Ok(d);
|
||
}
|
||
|
||
[HttpPost("UpdateDispatch")]
|
||
public async Task<IActionResult> UpdateDispatch([FromBody] UpdateDispatch_DTO model)
|
||
{
|
||
try
|
||
{
|
||
var dispatch = await _db.Dispatches.FindAsync(model.Id);
|
||
if (dispatch == null)
|
||
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
|
||
|
||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||
|
||
if (dispatch.Status != model.Status)
|
||
{
|
||
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
|
||
{
|
||
WorkOrderId = dispatch.WorkOrderId ?? 0,
|
||
UserId = userId,
|
||
FieldName = $"Dispatch {dispatch.DispatchNumber} Status",
|
||
OldValue = dispatch.Status ?? "",
|
||
NewValue = model.Status ?? "",
|
||
Action = "dispatch_status_change",
|
||
CreatedAt = DateTime.UtcNow
|
||
});
|
||
}
|
||
|
||
dispatch.Status = model.Status;
|
||
dispatch.NTEAmount = model.NTEAmount;
|
||
dispatch.ScheduledDate = model.ScheduledDate;
|
||
if (dispatch.EstimatedArrivalAt != model.EstimatedArrivalAt)
|
||
{
|
||
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
|
||
{
|
||
WorkOrderId = dispatch.WorkOrderId ?? 0,
|
||
UserId = userId,
|
||
FieldName = "Vendor ETA",
|
||
OldValue = dispatch.EstimatedArrivalAt?.ToUniversalTime().ToString("O") ?? "",
|
||
NewValue = model.EstimatedArrivalAt?.ToUniversalTime().ToString("O") ?? "",
|
||
Action = model.EtaManualOverride ? "vendor_eta_manual_override" : "vendor_eta_change",
|
||
CreatedAt = DateTime.UtcNow
|
||
});
|
||
}
|
||
dispatch.EstimatedArrivalAt = model.EstimatedArrivalAt;
|
||
dispatch.EtaManualOverride = model.EtaManualOverride;
|
||
dispatch.CompletedDate = model.CompletedDate;
|
||
dispatch.Description = model.Description;
|
||
|
||
if (model.Status == "Completed" && dispatch.CompletedDate == null)
|
||
dispatch.CompletedDate = DateTime.UtcNow;
|
||
|
||
await _db.SaveChangesAsync();
|
||
return Ok(new { message = "Dispatch updated" });
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||
}
|
||
}
|
||
|
||
[HttpPost("AddDispatchComment")]
|
||
public async Task<IActionResult> AddDispatchComment([FromBody] DispatchComment_DTO model)
|
||
{
|
||
try
|
||
{
|
||
var dispatch = await _db.Dispatches.Include(d => d.Vendor).Include(d => d.WorkOrder)
|
||
.FirstOrDefaultAsync(d => d.Id == model.DispatchId);
|
||
if (dispatch == null)
|
||
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
|
||
|
||
var userId = _httpContext?.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "";
|
||
var user = !string.IsNullOrEmpty(userId) ? await _db.Users.FindAsync(userId) : null;
|
||
var senderName = user != null ? (user.FirstName + " " + user.LastName).Trim() : "";
|
||
|
||
var comment = new Comments
|
||
{
|
||
CreatedDate = DateTime.UtcNow,
|
||
UserId = userId,
|
||
WorkerOrderId = dispatch.WorkOrderId,
|
||
DispatchId = dispatch.Id,
|
||
Commenttext = model.Text,
|
||
CommentType = "dispatcher",
|
||
Commenter = !string.IsNullOrWhiteSpace(senderName) ? senderName : null,
|
||
};
|
||
_db.Comments.Add(comment);
|
||
await _db.SaveChangesAsync();
|
||
|
||
if (model.SendEmail && dispatch.Vendor?.Email != null && dispatch.WorkOrder != null)
|
||
{
|
||
var subject = $"[WO-{dispatch.WorkOrder.InternalWONumber}-{dispatch.DispatchNumber}] {dispatch.WorkOrder.WorkerOrderTitle}";
|
||
var html = $"<p>{model.Text}</p><br/><p style='color:#666;font-size:13px;'>{senderName}<br/>Sea Haven Industries</p><hr/><p style='color:#999;font-size:12px;'>Reply to this email to respond.</p>";
|
||
await _sendMessage.SendDispatchEmail(dispatch.Vendor.Email, subject, html, dispatch.ReplyToAddress);
|
||
}
|
||
return Ok(new
|
||
{
|
||
comment.Id,
|
||
comment.CreatedDate,
|
||
comment.Commenttext,
|
||
comment.CommentType,
|
||
userName = user != null ? (user.FirstName + " " + user.LastName).Trim() : ""
|
||
});
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||
}
|
||
}
|
||
|
||
private static string? ExtractZip(string? address)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(address)) return null;
|
||
var match = System.Text.RegularExpressions.Regex.Match(address, @"\b(\d{5})(?:-\d{4})?\b");
|
||
return match.Success ? match.Groups[1].Value : null;
|
||
}
|
||
|
||
[HttpPost("VerifyDispatch")]
|
||
public async Task<IActionResult> VerifyDispatch(int dispatchId)
|
||
{
|
||
try
|
||
{
|
||
var dispatch = await _db.Dispatches.FindAsync(dispatchId);
|
||
if (dispatch == null)
|
||
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
|
||
|
||
var incompleteItems = await _db.DispatchChecklistItems
|
||
.Where(c => c.DispatchId == dispatchId && !c.IsCompleted)
|
||
.CountAsync();
|
||
|
||
var signoffCount = await _db.DispatchSignoffs
|
||
.Where(s => s.DispatchId == dispatchId)
|
||
.CountAsync();
|
||
|
||
var missing = new List<string>();
|
||
if (incompleteItems > 0)
|
||
missing.Add($"{incompleteItems} checklist item(s) incomplete");
|
||
if (signoffCount < 2)
|
||
{
|
||
var existingTypes = await _db.DispatchSignoffs
|
||
.Where(s => s.DispatchId == dispatchId)
|
||
.Select(s => s.SignoffType)
|
||
.ToListAsync();
|
||
if (!existingTypes.Contains("Customer")) missing.Add("Missing Customer sign-off");
|
||
if (!existingTypes.Contains("Vendor")) missing.Add("Missing Vendor sign-off");
|
||
}
|
||
|
||
if (missing.Count > 0)
|
||
return BadRequest(new { status = "Error", message = "Cannot verify", missing });
|
||
|
||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||
var user = userId != null ? await _db.Users.FindAsync(userId) : null;
|
||
var userName = user != null ? (user.FirstName + " " + user.LastName).Trim() : "";
|
||
|
||
dispatch.Status = "Verified";
|
||
dispatch.VerifiedBy = userName;
|
||
dispatch.VerifiedAt = DateTime.UtcNow;
|
||
|
||
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
|
||
{
|
||
WorkOrderId = dispatch.WorkOrderId ?? 0,
|
||
UserId = userId,
|
||
FieldName = $"Dispatch {dispatch.DispatchNumber}",
|
||
OldValue = dispatch.Status ?? "",
|
||
NewValue = "Verified",
|
||
Action = "dispatch_verified",
|
||
CreatedAt = DateTime.UtcNow
|
||
});
|
||
|
||
await _db.SaveChangesAsync();
|
||
return Ok(new { message = "Dispatch verified", verifiedBy = userName, verifiedAt = dispatch.VerifiedAt });
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||
}
|
||
}
|
||
|
||
[HttpPost("AddDispatchSignoff")]
|
||
public async Task<IActionResult> AddDispatchSignoff([FromBody] DispatchSignoff_DTO model)
|
||
{
|
||
try
|
||
{
|
||
var dispatch = await _db.Dispatches.FindAsync(model.DispatchId);
|
||
if (dispatch == null)
|
||
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
|
||
|
||
var existing = await _db.DispatchSignoffs
|
||
.AnyAsync(s => s.DispatchId == model.DispatchId && s.SignoffType == model.SignoffType);
|
||
if (existing)
|
||
return BadRequest(new Response { Status = "Error", Message = $"{model.SignoffType} sign-off already exists" });
|
||
|
||
var signoff = new DispatchSignoff
|
||
{
|
||
DispatchId = model.DispatchId,
|
||
SignoffType = model.SignoffType,
|
||
Name = model.Name,
|
||
Signature = model.Signature,
|
||
SignatureMethod = model.SignatureMethod,
|
||
SignedAt = DateTime.UtcNow
|
||
};
|
||
_db.DispatchSignoffs.Add(signoff);
|
||
await _db.SaveChangesAsync();
|
||
|
||
return Ok(new { signoff.Id, signoff.SignoffType, signoff.Name, signoff.SignedAt, signoff.SignatureMethod });
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||
}
|
||
}
|
||
|
||
[HttpPost("UpdateChecklistItem")]
|
||
public async Task<IActionResult> UpdateChecklistItem([FromBody] ChecklistItemUpdate_DTO model)
|
||
{
|
||
var item = await _dispatchDataService.GetChecklistItemByIdAsync(model.Id);
|
||
if (item == null)
|
||
return NotFound(new Response { Status = "Error", Message = "Checklist item not found" });
|
||
|
||
item.IsCompleted = model.IsCompleted;
|
||
item.CompletedBy = model.IsCompleted ? model.CompletedBy : null;
|
||
item.CompletedAt = model.IsCompleted ? DateTime.UtcNow : null;
|
||
await _dispatchDataService.UpdateChecklistItemAsync(item);
|
||
return Ok(new { message = "Updated", item.Id, item.IsCompleted, item.CompletedBy, item.CompletedAt });
|
||
}
|
||
|
||
[HttpPost("AddChecklistItem")]
|
||
public async Task<IActionResult> AddChecklistItem([FromBody] AddChecklistItem_DTO model)
|
||
{
|
||
var checklistItems = await _dispatchDataService.GetChecklistItemsAsync(model.DispatchId);
|
||
var maxSort = checklistItems.Any() ? checklistItems.Max(c => c.SortOrder) : -1;
|
||
|
||
var item = new DispatchChecklistItem
|
||
{
|
||
DispatchId = model.DispatchId,
|
||
WorkOrderId = model.WorkOrderId,
|
||
ItemText = model.ItemText,
|
||
SortOrder = maxSort + 1,
|
||
IsCompleted = false
|
||
};
|
||
await _dispatchDataService.AddChecklistItemAsync(item);
|
||
return Ok(new { item.Id, item.ItemText, item.SortOrder, item.IsCompleted });
|
||
}
|
||
|
||
private async Task<string> GenerateInternalWONumber()
|
||
{
|
||
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)!);
|
||
|
||
using (var stream = System.IO.File.Create(fullPath))
|
||
{
|
||
await file.CopyToAsync(stream);
|
||
}
|
||
|
||
var request = _httpContext.HttpContext!.Request;
|
||
var domain = $"{request.Scheme}://{request.Host}";
|
||
return $"{domain}/{uploadPath.Replace("\\", "/")}/{uniqueFileName}";
|
||
}
|
||
|
||
public class WorkorderFilterVM
|
||
{
|
||
public int? Id { get; set; }
|
||
public int? LocationId { get; set; }
|
||
public string? WorkerOrderNumber { get; set; }
|
||
public string? BillingAddress { get; set; } = "Amazon.com Services LLC 410 Terry Ave,North Seattle, Washington, 98109";
|
||
|
||
public string? WorkerOrderTitle { get; set; }
|
||
public string? LocationName { get; set; }
|
||
public string? AssignToUserName { get; set; }
|
||
public string? Priority { get; set; }
|
||
public string? Status { get; set; }
|
||
public DateTime? DueDate { get; set; }
|
||
public string FormattedDueDate
|
||
{
|
||
get { return DueDate.HasValue ? DueDate.Value.ToString("MM/dd/yyyy") : ""; }
|
||
}
|
||
public string? Dispatcher { get; set; }
|
||
public string? Scheduler { get; set; }
|
||
public string? PO { get; set; }
|
||
public string? TT { get; set; }
|
||
}
|
||
}
|
||
}
|