mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 09:33:13 +00:00
Expose POST /api/workorders/board and POST /api/workorders/{id}/cancel for SHOC wizard/inline creation and soft cancel, with field locks, WO# normalization, and Admin-only hard delete.
1475 lines
66 KiB
C#
1475 lines
66 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.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.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;
|
|
}
|
|
|
|
[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);
|
|
}
|
|
|
|
[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)
|
|
{
|
|
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)
|
|
{
|
|
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
|
|
});
|
|
}
|
|
}
|
|
|
|
[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.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) continue;
|
|
|
|
var poNumber = $"VPO-{nextPO:D5}";
|
|
nextPO++;
|
|
var dispatchNumber = $"DSP-{nextDSP:D5}";
|
|
nextDSP++;
|
|
|
|
var replyTo = $"wo-{primaryWO.InternalWONumber}-{dispatchNumber}@int.seahaven.com";
|
|
|
|
var dispatch = new Dispatch
|
|
{
|
|
WorkOrderId = primaryWO.Id,
|
|
VendorId = vendorId,
|
|
DispatchNumber = dispatchNumber,
|
|
PONumber = poNumber,
|
|
NTEAmount = model.NTEAmount,
|
|
Description = model.Description,
|
|
Status = "Sent",
|
|
ScheduledDate = model.ScheduledDate,
|
|
DispatchedAt = DateTime.UtcNow,
|
|
ReplyToAddress = replyTo,
|
|
DispatchWorkOrders = workOrders.Select(wo => new DispatchWorkOrder { WorkOrderId = wo.Id }).ToList()
|
|
};
|
|
|
|
var checklistItems = new List<DispatchChecklistItem>();
|
|
if (model.TaskListTemplateId.HasValue)
|
|
{
|
|
var template = await _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
|
|
});
|
|
}
|
|
|
|
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.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.CompletedDate,
|
|
x.DispatchedAt,
|
|
x.EmailSent,
|
|
x.WorkOrderId,
|
|
x.AcknowledgedAt,
|
|
x.VerifiedBy,
|
|
x.VerifiedAt,
|
|
woNumber = x.WorkOrder != null ? x.WorkOrder.InternalWONumber : "",
|
|
woTitle = x.WorkOrder != null ? x.WorkOrder.WorkerOrderTitle : "",
|
|
vendorName = x.Vendor != null ? x.Vendor.CompanyName : "",
|
|
vendorEmail = x.Vendor != null ? x.Vendor.Email : "",
|
|
vendorId = x.VendorId,
|
|
workOrders = _db.DispatchWorkOrders
|
|
.Where(dwo => dwo.DispatchId == x.Id)
|
|
.Select(dwo => new {
|
|
dwo.WorkOrderId,
|
|
woNumber = dwo.WorkOrder != null ? dwo.WorkOrder.InternalWONumber : "",
|
|
woTitle = dwo.WorkOrder != null ? dwo.WorkOrder.WorkerOrderTitle : ""
|
|
}).ToList(),
|
|
checklistItems = _db.DispatchChecklistItems
|
|
.Where(ci => ci.DispatchId == x.Id)
|
|
.OrderBy(ci => ci.SortOrder)
|
|
.Select(ci => new {
|
|
ci.Id,
|
|
ci.ItemText,
|
|
ci.IsCompleted,
|
|
ci.CompletedBy,
|
|
ci.CompletedAt,
|
|
ci.SortOrder,
|
|
ci.WorkOrderId
|
|
}).ToList(),
|
|
signoffs = _db.DispatchSignoffs
|
|
.Where(s => s.DispatchId == x.Id)
|
|
.Select(s => new {
|
|
s.Id,
|
|
s.SignoffType,
|
|
s.Name,
|
|
s.Signature,
|
|
s.SignatureMethod,
|
|
s.SignedAt
|
|
}).ToList(),
|
|
comments = _db.Comments
|
|
.Where(c => c.DispatchId == x.Id)
|
|
.OrderBy(c => c.CreatedDate)
|
|
.Select(c => new
|
|
{
|
|
c.Id,
|
|
c.CreatedDate,
|
|
c.Commenttext,
|
|
c.Commenter,
|
|
c.CommentType,
|
|
userName = c.ApplicationUser != null ? c.ApplicationUser.FirstName + " " + c.ApplicationUser.LastName : ""
|
|
}).ToList()
|
|
}).FirstOrDefaultAsync();
|
|
|
|
if (d == null)
|
|
return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
|
|
return Ok(d);
|
|
}
|
|
|
|
[HttpPost("UpdateDispatch")]
|
|
public async Task<IActionResult> UpdateDispatch([FromBody] UpdateDispatch_DTO model)
|
|
{
|
|
try
|
|
{
|
|
var dispatch = await _db.Dispatches.FindAsync(model.Id);
|
|
if (dispatch == null)
|
|
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
|
|
|
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
|
|
if (dispatch.Status != model.Status)
|
|
{
|
|
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
|
|
{
|
|
WorkOrderId = dispatch.WorkOrderId ?? 0,
|
|
UserId = userId,
|
|
FieldName = $"Dispatch {dispatch.DispatchNumber} Status",
|
|
OldValue = dispatch.Status ?? "",
|
|
NewValue = model.Status ?? "",
|
|
Action = "dispatch_status_change",
|
|
CreatedAt = DateTime.UtcNow
|
|
});
|
|
}
|
|
|
|
dispatch.Status = model.Status;
|
|
dispatch.NTEAmount = model.NTEAmount;
|
|
dispatch.ScheduledDate = model.ScheduledDate;
|
|
dispatch.CompletedDate = model.CompletedDate;
|
|
dispatch.Description = model.Description;
|
|
|
|
if (model.Status == "Completed" && dispatch.CompletedDate == null)
|
|
dispatch.CompletedDate = DateTime.UtcNow;
|
|
|
|
await _db.SaveChangesAsync();
|
|
return Ok(new { message = "Dispatch updated" });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
|
}
|
|
}
|
|
|
|
[HttpPost("AddDispatchComment")]
|
|
public async Task<IActionResult> AddDispatchComment([FromBody] DispatchComment_DTO model)
|
|
{
|
|
try
|
|
{
|
|
var dispatch = await _db.Dispatches.Include(d => d.Vendor).Include(d => d.WorkOrder)
|
|
.FirstOrDefaultAsync(d => d.Id == model.DispatchId);
|
|
if (dispatch == null)
|
|
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
|
|
|
|
var userId = _httpContext?.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "";
|
|
var user = !string.IsNullOrEmpty(userId) ? await _db.Users.FindAsync(userId) : null;
|
|
var senderName = user != null ? (user.FirstName + " " + user.LastName).Trim() : "";
|
|
|
|
var comment = new Comments
|
|
{
|
|
CreatedDate = DateTime.UtcNow,
|
|
UserId = userId,
|
|
WorkerOrderId = dispatch.WorkOrderId,
|
|
DispatchId = dispatch.Id,
|
|
Commenttext = model.Text,
|
|
CommentType = "dispatcher",
|
|
Commenter = !string.IsNullOrWhiteSpace(senderName) ? senderName : null,
|
|
};
|
|
_db.Comments.Add(comment);
|
|
await _db.SaveChangesAsync();
|
|
|
|
if (model.SendEmail && dispatch.Vendor?.Email != null && dispatch.WorkOrder != null)
|
|
{
|
|
var subject = $"[WO-{dispatch.WorkOrder.InternalWONumber}-{dispatch.DispatchNumber}] {dispatch.WorkOrder.WorkerOrderTitle}";
|
|
var html = $"<p>{model.Text}</p><br/><p style='color:#666;font-size:13px;'>{senderName}<br/>Sea Haven Industries</p><hr/><p style='color:#999;font-size:12px;'>Reply to this email to respond.</p>";
|
|
await _sendMessage.SendDispatchEmail(dispatch.Vendor.Email, subject, html, dispatch.ReplyToAddress);
|
|
}
|
|
return Ok(new
|
|
{
|
|
comment.Id,
|
|
comment.CreatedDate,
|
|
comment.Commenttext,
|
|
comment.CommentType,
|
|
userName = user != null ? (user.FirstName + " " + user.LastName).Trim() : ""
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
|
}
|
|
}
|
|
|
|
private static string? ExtractZip(string? address)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(address)) return null;
|
|
var match = System.Text.RegularExpressions.Regex.Match(address, @"\b(\d{5})(?:-\d{4})?\b");
|
|
return match.Success ? match.Groups[1].Value : null;
|
|
}
|
|
|
|
[HttpPost("VerifyDispatch")]
|
|
public async Task<IActionResult> VerifyDispatch(int dispatchId)
|
|
{
|
|
try
|
|
{
|
|
var dispatch = await _db.Dispatches.FindAsync(dispatchId);
|
|
if (dispatch == null)
|
|
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
|
|
|
|
var incompleteItems = await _db.DispatchChecklistItems
|
|
.Where(c => c.DispatchId == dispatchId && !c.IsCompleted)
|
|
.CountAsync();
|
|
|
|
var signoffCount = await _db.DispatchSignoffs
|
|
.Where(s => s.DispatchId == dispatchId)
|
|
.CountAsync();
|
|
|
|
var missing = new List<string>();
|
|
if (incompleteItems > 0)
|
|
missing.Add($"{incompleteItems} checklist item(s) incomplete");
|
|
if (signoffCount < 2)
|
|
{
|
|
var existingTypes = await _db.DispatchSignoffs
|
|
.Where(s => s.DispatchId == dispatchId)
|
|
.Select(s => s.SignoffType)
|
|
.ToListAsync();
|
|
if (!existingTypes.Contains("Customer")) missing.Add("Missing Customer sign-off");
|
|
if (!existingTypes.Contains("Vendor")) missing.Add("Missing Vendor sign-off");
|
|
}
|
|
|
|
if (missing.Count > 0)
|
|
return BadRequest(new { status = "Error", message = "Cannot verify", missing });
|
|
|
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
var user = userId != null ? await _db.Users.FindAsync(userId) : null;
|
|
var userName = user != null ? (user.FirstName + " " + user.LastName).Trim() : "";
|
|
|
|
dispatch.Status = "Verified";
|
|
dispatch.VerifiedBy = userName;
|
|
dispatch.VerifiedAt = DateTime.UtcNow;
|
|
|
|
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
|
|
{
|
|
WorkOrderId = dispatch.WorkOrderId ?? 0,
|
|
UserId = userId,
|
|
FieldName = $"Dispatch {dispatch.DispatchNumber}",
|
|
OldValue = dispatch.Status ?? "",
|
|
NewValue = "Verified",
|
|
Action = "dispatch_verified",
|
|
CreatedAt = DateTime.UtcNow
|
|
});
|
|
|
|
await _db.SaveChangesAsync();
|
|
return Ok(new { message = "Dispatch verified", verifiedBy = userName, verifiedAt = dispatch.VerifiedAt });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
|
}
|
|
}
|
|
|
|
[HttpPost("AddDispatchSignoff")]
|
|
public async Task<IActionResult> AddDispatchSignoff([FromBody] DispatchSignoff_DTO model)
|
|
{
|
|
try
|
|
{
|
|
var dispatch = await _db.Dispatches.FindAsync(model.DispatchId);
|
|
if (dispatch == null)
|
|
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
|
|
|
|
var existing = await _db.DispatchSignoffs
|
|
.AnyAsync(s => s.DispatchId == model.DispatchId && s.SignoffType == model.SignoffType);
|
|
if (existing)
|
|
return BadRequest(new Response { Status = "Error", Message = $"{model.SignoffType} sign-off already exists" });
|
|
|
|
var signoff = new DispatchSignoff
|
|
{
|
|
DispatchId = model.DispatchId,
|
|
SignoffType = model.SignoffType,
|
|
Name = model.Name,
|
|
Signature = model.Signature,
|
|
SignatureMethod = model.SignatureMethod,
|
|
SignedAt = DateTime.UtcNow
|
|
};
|
|
_db.DispatchSignoffs.Add(signoff);
|
|
await _db.SaveChangesAsync();
|
|
|
|
return Ok(new { signoff.Id, signoff.SignoffType, signoff.Name, signoff.SignedAt, signoff.SignatureMethod });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
|
}
|
|
}
|
|
|
|
[HttpPost("UpdateChecklistItem")]
|
|
public async Task<IActionResult> UpdateChecklistItem([FromBody] ChecklistItemUpdate_DTO model)
|
|
{
|
|
var item = await _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; }
|
|
}
|
|
}
|
|
}
|