Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
using Api.SeaHavenIndustries.DTOs;
|
|
|
|
|
|
using Data.SeaHavenIndustries;
|
2026-05-14 11:00:12 -05:00
|
|
|
|
using FluentValidation;
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
using Microsoft.AspNetCore.Authorization;
|
|
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
2026-05-06 10:49:33 -05:00
|
|
|
|
using SeaHaven.Services.Interfaces;
|
|
|
|
|
|
using SeaHaven.Services.DTOs;
|
2026-05-14 11:00:12 -05:00
|
|
|
|
using System.Security.Claims;
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
|
|
|
|
|
|
namespace Api.SeaHavenIndustries.Controllers
|
|
|
|
|
|
{
|
|
|
|
|
|
[Authorize]
|
|
|
|
|
|
[ApiController]
|
|
|
|
|
|
[Route("api/[controller]")]
|
|
|
|
|
|
[Route("api/vendors")]
|
|
|
|
|
|
public class VendorController : Controller
|
|
|
|
|
|
{
|
2026-05-06 10:49:33 -05:00
|
|
|
|
private readonly IVendorService _vendorService;
|
|
|
|
|
|
private readonly ApplicationDbContext _db; // Keep for complex queries like nearby vendors
|
2026-04-17 11:56:06 -04:00
|
|
|
|
private readonly Helper.ZipCodeDistance _zipDistance;
|
2026-04-20 12:01:53 -04:00
|
|
|
|
private readonly Helper.VendorPortalTokenService _vendorTokens;
|
|
|
|
|
|
private readonly IConfiguration _config;
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
|
2026-05-06 10:49:33 -05:00
|
|
|
|
public VendorController(IVendorService vendorService, ApplicationDbContext db, Helper.ZipCodeDistance zipDistance, Helper.VendorPortalTokenService vendorTokens, IConfiguration config)
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
{
|
2026-05-06 10:49:33 -05:00
|
|
|
|
_vendorService = vendorService;
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
_db = db;
|
2026-04-17 11:56:06 -04:00
|
|
|
|
_zipDistance = zipDistance;
|
2026-04-20 12:01:53 -04:00
|
|
|
|
_vendorTokens = vendorTokens;
|
|
|
|
|
|
_config = config;
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[HttpGet("GetVendorList")]
|
2026-07-23 12:55:47 -03:00
|
|
|
|
public async Task<IActionResult> GetVendorList(
|
|
|
|
|
|
string? search = "",
|
|
|
|
|
|
int page = 1,
|
|
|
|
|
|
int pageSize = 10,
|
|
|
|
|
|
bool? isActive = true,
|
|
|
|
|
|
[FromQuery] string[]? companies = null,
|
|
|
|
|
|
[FromQuery] string[]? trades = null,
|
|
|
|
|
|
[FromQuery] string[]? locations = null,
|
|
|
|
|
|
[FromQuery] string[]? jobBuckets = null)
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
{
|
2026-07-23 12:55:47 -03:00
|
|
|
|
var pagedResult = await _vendorService.GetVendorsPagedAsync(
|
|
|
|
|
|
page,
|
|
|
|
|
|
pageSize,
|
|
|
|
|
|
search,
|
|
|
|
|
|
isActive,
|
|
|
|
|
|
companies,
|
|
|
|
|
|
trades,
|
|
|
|
|
|
locations,
|
|
|
|
|
|
jobBuckets);
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
|
2026-05-06 10:49:33 -05:00
|
|
|
|
var data = pagedResult.Items.Select(v => new
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
{
|
2026-05-06 10:49:33 -05:00
|
|
|
|
v.Id,
|
|
|
|
|
|
CompanyName = v.Name,
|
2026-07-23 17:02:40 -03:00
|
|
|
|
v.CompanyId,
|
2026-07-23 12:55:47 -03:00
|
|
|
|
v.ContactName,
|
2026-05-06 10:49:33 -05:00
|
|
|
|
v.Email,
|
2026-07-23 12:55:47 -03:00
|
|
|
|
v.Phone,
|
|
|
|
|
|
v.CompanyPhone,
|
2026-07-23 17:02:40 -03:00
|
|
|
|
v.PreferredContact,
|
2026-07-23 12:55:47 -03:00
|
|
|
|
v.Address,
|
|
|
|
|
|
v.City,
|
|
|
|
|
|
v.State,
|
|
|
|
|
|
Zip = v.Zipcode,
|
|
|
|
|
|
v.TradeSpecialties,
|
|
|
|
|
|
v.GoogleMapsUrl,
|
|
|
|
|
|
v.Notes,
|
|
|
|
|
|
v.IsActive,
|
|
|
|
|
|
v.TotalJobs
|
2026-05-06 10:49:33 -05:00
|
|
|
|
});
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
|
|
|
|
|
|
return Ok(new Pagination_DTO
|
|
|
|
|
|
{
|
|
|
|
|
|
Data = data,
|
|
|
|
|
|
PageNumber = page,
|
|
|
|
|
|
PageSize = pageSize,
|
2026-05-06 10:49:33 -05:00
|
|
|
|
TotalCount = pagedResult.TotalCount,
|
|
|
|
|
|
TotalPages = (int)Math.Ceiling(pagedResult.TotalCount / (double)pageSize)
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[HttpGet("{id}")]
|
2026-05-14 11:00:12 -05:00
|
|
|
|
[HttpGet("GetById")]
|
2026-07-23 12:55:47 -03:00
|
|
|
|
public async Task<IActionResult> GetVendorById([FromRoute] int? id, [FromQuery(Name = "id")] int? queryId)
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
{
|
2026-07-23 12:55:47 -03:00
|
|
|
|
var vendorId = id ?? queryId;
|
|
|
|
|
|
if (vendorId == null)
|
2026-05-14 11:00:12 -05:00
|
|
|
|
return BadRequest(new Response { Status = "Error", Message = "Id is required" });
|
|
|
|
|
|
|
2026-07-23 12:55:47 -03:00
|
|
|
|
var vendor = await _vendorService.GetVendorByIdAsync(vendorId.Value);
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
|
|
|
|
|
|
if (vendor == null)
|
|
|
|
|
|
return NotFound(new Response { Status = "Error", Message = "Vendor not found" });
|
|
|
|
|
|
|
2026-07-23 12:55:47 -03:00
|
|
|
|
return Ok(new
|
|
|
|
|
|
{
|
|
|
|
|
|
vendor.Id,
|
|
|
|
|
|
CompanyName = vendor.Name,
|
2026-07-23 17:02:40 -03:00
|
|
|
|
vendor.CompanyId,
|
2026-07-23 12:55:47 -03:00
|
|
|
|
vendor.ContactName,
|
|
|
|
|
|
vendor.Email,
|
|
|
|
|
|
vendor.Phone,
|
|
|
|
|
|
vendor.CompanyPhone,
|
2026-07-23 17:02:40 -03:00
|
|
|
|
vendor.PreferredContact,
|
2026-07-23 12:55:47 -03:00
|
|
|
|
vendor.Address,
|
|
|
|
|
|
vendor.City,
|
|
|
|
|
|
vendor.State,
|
|
|
|
|
|
Zip = vendor.Zipcode,
|
|
|
|
|
|
vendor.TradeSpecialties,
|
|
|
|
|
|
vendor.GoogleMapsUrl,
|
|
|
|
|
|
vendor.Notes,
|
|
|
|
|
|
vendor.IsActive,
|
|
|
|
|
|
vendor.TotalJobs
|
|
|
|
|
|
});
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[HttpPost]
|
2026-05-14 11:00:12 -05:00
|
|
|
|
[HttpPost("Create")]
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
public async Task<IActionResult> Create([FromBody] Vendor_DTO model)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-05-06 10:49:33 -05:00
|
|
|
|
var createDto = new CreateVendorDTO
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
{
|
2026-07-23 12:55:47 -03:00
|
|
|
|
Name = model.CompanyName ?? throw new ArgumentException("CompanyName is required"),
|
2026-07-23 17:02:40 -03:00
|
|
|
|
CompanyId = model.CompanyId,
|
2026-07-23 12:55:47 -03:00
|
|
|
|
ContactName = model.ContactName,
|
|
|
|
|
|
Email = model.Email,
|
|
|
|
|
|
Phone = model.Phone,
|
|
|
|
|
|
CompanyPhone = model.CompanyPhone,
|
2026-07-23 17:02:40 -03:00
|
|
|
|
PreferredContact = model.PreferredContact,
|
2026-07-23 12:55:47 -03:00
|
|
|
|
Address = model.Address,
|
|
|
|
|
|
City = model.City,
|
|
|
|
|
|
State = model.State,
|
|
|
|
|
|
Zipcode = model.Zip,
|
|
|
|
|
|
TradeSpecialties = model.TradeSpecialties,
|
|
|
|
|
|
GoogleMapsUrl = model.GoogleMapsUrl,
|
|
|
|
|
|
Notes = model.Notes,
|
|
|
|
|
|
IsActive = model.IsActive ?? true
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
};
|
2026-05-06 10:49:33 -05:00
|
|
|
|
|
2026-05-14 11:00:12 -05:00
|
|
|
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
|
|
|
|
if (userId == null)
|
|
|
|
|
|
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
|
|
|
|
|
|
2026-05-06 10:49:33 -05:00
|
|
|
|
await _vendorService.CreateVendorAsync(createDto, userId);
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
return Ok(new DataResponse { Message = "Vendor Created", Status = "200" });
|
|
|
|
|
|
}
|
2026-05-14 11:00:12 -05:00
|
|
|
|
catch (ValidationException vex)
|
|
|
|
|
|
{
|
|
|
|
|
|
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
|
|
|
|
|
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
|
|
|
|
|
}
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-05-14 11:00:12 -05:00
|
|
|
|
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[HttpPut("{id}")]
|
2026-05-14 11:00:12 -05:00
|
|
|
|
[HttpPost("Update")]
|
|
|
|
|
|
public async Task<IActionResult> Update([FromRoute] int? id, [FromBody] EditVendor_DTO model)
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-05-14 11:00:12 -05:00
|
|
|
|
// For named route, id comes from model
|
|
|
|
|
|
int vendorId = id ?? model.Id;
|
|
|
|
|
|
if (vendorId == 0)
|
|
|
|
|
|
return BadRequest(new Response { Status = "Error", Message = "Vendor Id is required" });
|
|
|
|
|
|
|
2026-05-06 10:49:33 -05:00
|
|
|
|
var updateDto = new UpdateVendorDTO
|
|
|
|
|
|
{
|
2026-07-23 12:55:47 -03:00
|
|
|
|
Name = model.CompanyName,
|
2026-07-23 17:02:40 -03:00
|
|
|
|
CompanyId = model.CompanyId,
|
2026-07-23 12:55:47 -03:00
|
|
|
|
ContactName = model.ContactName,
|
|
|
|
|
|
Email = model.Email,
|
|
|
|
|
|
Phone = model.Phone,
|
|
|
|
|
|
CompanyPhone = model.CompanyPhone,
|
2026-07-23 17:02:40 -03:00
|
|
|
|
PreferredContact = model.PreferredContact,
|
2026-07-23 12:55:47 -03:00
|
|
|
|
Address = model.Address,
|
|
|
|
|
|
City = model.City,
|
|
|
|
|
|
State = model.State,
|
|
|
|
|
|
Zipcode = model.Zip,
|
|
|
|
|
|
TradeSpecialties = model.TradeSpecialties,
|
|
|
|
|
|
GoogleMapsUrl = model.GoogleMapsUrl,
|
|
|
|
|
|
Notes = model.Notes,
|
|
|
|
|
|
IsActive = model.IsActive
|
2026-05-06 10:49:33 -05:00
|
|
|
|
};
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
|
2026-05-14 11:00:12 -05:00
|
|
|
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
|
|
|
|
if (userId == null)
|
|
|
|
|
|
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
|
|
|
|
|
|
|
|
|
|
|
await _vendorService.UpdateVendorAsync(vendorId, updateDto, userId);
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
return Ok(new DataResponse { Message = "Vendor Updated", Status = "200" });
|
|
|
|
|
|
}
|
2026-05-14 11:00:12 -05:00
|
|
|
|
catch (ValidationException vex)
|
|
|
|
|
|
{
|
|
|
|
|
|
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
|
|
|
|
|
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
|
|
|
|
|
}
|
2026-07-23 17:02:40 -03:00
|
|
|
|
catch (VendorDeactivationBlockedException dbex)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Conflict(new
|
|
|
|
|
|
{
|
|
|
|
|
|
Status = "Conflict",
|
|
|
|
|
|
Message = dbex.Message,
|
|
|
|
|
|
OpenWorkOrders = dbex.OpenWorkOrders
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-07-23 12:55:47 -03:00
|
|
|
|
catch (InvalidOperationException)
|
2026-05-14 11:00:12 -05:00
|
|
|
|
{
|
|
|
|
|
|
return NotFound(new Response { Status = "Error", Message = "Vendor not found" });
|
|
|
|
|
|
}
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-05-14 11:00:12 -05:00
|
|
|
|
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[HttpDelete("{id}")]
|
2026-05-14 11:00:12 -05:00
|
|
|
|
[HttpPost("Delete")]
|
2026-07-23 12:55:47 -03:00
|
|
|
|
public async Task<IActionResult> Delete([FromRoute] int? id, [FromQuery(Name = "id")] int? queryId = null)
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
{
|
2026-05-14 11:00:12 -05:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
// Support both route parameter and query string
|
|
|
|
|
|
int vendorId = id ?? queryId ?? 0;
|
|
|
|
|
|
if (vendorId == 0)
|
|
|
|
|
|
return BadRequest(new Response { Status = "Error", Message = "Vendor Id is required" });
|
|
|
|
|
|
|
|
|
|
|
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
|
|
|
|
if (userId == null)
|
|
|
|
|
|
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
|
|
|
|
|
|
|
|
|
|
|
await _vendorService.DeleteVendorAsync(vendorId, userId);
|
2026-07-23 12:55:47 -03:00
|
|
|
|
return Ok(new DataResponse { Message = "Vendor Deactivated", Status = "200" });
|
2026-05-14 11:00:12 -05:00
|
|
|
|
}
|
2026-07-23 17:02:40 -03:00
|
|
|
|
catch (VendorDeactivationBlockedException dbex)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Conflict(new
|
|
|
|
|
|
{
|
|
|
|
|
|
Status = "Conflict",
|
|
|
|
|
|
Message = dbex.Message,
|
|
|
|
|
|
OpenWorkOrders = dbex.OpenWorkOrders
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (InvalidOperationException)
|
|
|
|
|
|
{
|
|
|
|
|
|
return NotFound(new Response { Status = "Error", Message = "Vendor not found" });
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[HttpGet("{id:int}/deactivation-impact")]
|
|
|
|
|
|
public async Task<IActionResult> GetDeactivationImpact(int id)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var impact = await _vendorService.GetDeactivationImpactAsync(id);
|
|
|
|
|
|
return Ok(impact);
|
|
|
|
|
|
}
|
2026-07-23 12:55:47 -03:00
|
|
|
|
catch (InvalidOperationException)
|
2026-05-14 11:00:12 -05:00
|
|
|
|
{
|
|
|
|
|
|
return NotFound(new Response { Status = "Error", Message = "Vendor not found" });
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
|
|
|
|
|
}
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 17:02:40 -03:00
|
|
|
|
[HttpPut("{id:int}/work-order-update")]
|
|
|
|
|
|
public async Task<IActionResult> UpdateFromWorkOrder(int id, [FromBody] WorkOrderVendorUpdate_DTO model)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
if (model == null || model.WorkOrderId <= 0)
|
|
|
|
|
|
return BadRequest(new Response { Status = "Error", Message = "A valid workOrderId is required" });
|
|
|
|
|
|
|
|
|
|
|
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
|
|
|
|
if (userId == null)
|
|
|
|
|
|
return Unauthorized(new Response { Status = "Error", Message = "User not authenticated" });
|
|
|
|
|
|
|
|
|
|
|
|
var dto = new WorkOrderVendorUpdateDTO
|
|
|
|
|
|
{
|
|
|
|
|
|
WorkOrderId = model.WorkOrderId,
|
|
|
|
|
|
ContactName = model.ContactName,
|
|
|
|
|
|
PreferredContact = model.PreferredContact,
|
|
|
|
|
|
Phone = model.Phone,
|
|
|
|
|
|
Email = model.Email,
|
|
|
|
|
|
Notes = model.Notes
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
var result = await _vendorService.UpdateVendorFromWorkOrderAsync(id, dto, userId);
|
|
|
|
|
|
return Ok(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (ValidationException vex)
|
|
|
|
|
|
{
|
|
|
|
|
|
var errors = string.Join(", ", vex.Errors.Select(e => e.ErrorMessage));
|
|
|
|
|
|
return BadRequest(new Response { Status = "Validation Error", Message = errors });
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (InvalidOperationException)
|
|
|
|
|
|
{
|
|
|
|
|
|
return NotFound(new Response { Status = "Error", Message = "Vendor or work order link not found" });
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
return StatusCode(500, new Response { Status = "Error", Message = ex.Message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
[HttpGet("Dropdown")]
|
2026-04-17 11:56:06 -04:00
|
|
|
|
public async Task<IActionResult> GetDropdown([FromQuery] string? trade = null, [FromQuery] string? siteZip = null)
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
{
|
|
|
|
|
|
var query = _db.Vendors.Where(v => v.IsActive);
|
|
|
|
|
|
|
|
|
|
|
|
var vendors = await query.OrderBy(v => v.CompanyName)
|
2026-04-17 11:56:06 -04:00
|
|
|
|
.Select(v => new
|
|
|
|
|
|
{
|
|
|
|
|
|
v.Id,
|
|
|
|
|
|
v.CompanyName,
|
2026-07-23 17:02:40 -03:00
|
|
|
|
v.ContactName,
|
|
|
|
|
|
v.PreferredContact,
|
2026-04-17 11:56:06 -04:00
|
|
|
|
v.TradeSpecialties,
|
|
|
|
|
|
v.Address,
|
|
|
|
|
|
v.City,
|
|
|
|
|
|
v.State,
|
|
|
|
|
|
v.Zip
|
|
|
|
|
|
})
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
2026-04-17 11:56:06 -04:00
|
|
|
|
var result = vendors.Select(v =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var distance = _zipDistance.GetDistanceMiles(siteZip, v.Zip);
|
|
|
|
|
|
var addr = new[] { v.Address, v.City, v.State, v.Zip }
|
|
|
|
|
|
.Where(s => !string.IsNullOrWhiteSpace(s));
|
|
|
|
|
|
return new
|
|
|
|
|
|
{
|
|
|
|
|
|
v.Id,
|
|
|
|
|
|
v.CompanyName,
|
2026-07-23 17:02:40 -03:00
|
|
|
|
v.ContactName,
|
|
|
|
|
|
v.PreferredContact,
|
2026-04-17 11:56:06 -04:00
|
|
|
|
v.TradeSpecialties,
|
|
|
|
|
|
address = string.Join(", ", addr),
|
|
|
|
|
|
distanceMiles = distance.HasValue ? Math.Round(distance.Value, 1) : (double?)null
|
|
|
|
|
|
};
|
|
|
|
|
|
}).ToList();
|
|
|
|
|
|
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
if (!string.IsNullOrWhiteSpace(trade))
|
|
|
|
|
|
{
|
2026-04-17 11:56:06 -04:00
|
|
|
|
var tradeMatched = result.Where(v => (v.TradeSpecialties ?? "").Contains(trade)).ToList();
|
|
|
|
|
|
var rest = result.Where(v => !(v.TradeSpecialties ?? "").Contains(trade)).ToList();
|
|
|
|
|
|
result = tradeMatched.Concat(rest).ToList();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(siteZip))
|
|
|
|
|
|
{
|
|
|
|
|
|
result = result.OrderBy(v => v.distanceMiles ?? 99999).ToList();
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-17 11:56:06 -04:00
|
|
|
|
return Ok(result);
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
}
|
2026-04-20 12:01:53 -04:00
|
|
|
|
|
2026-07-23 12:55:47 -03:00
|
|
|
|
[HttpGet("facets")]
|
|
|
|
|
|
public async Task<IActionResult> GetFacets([FromQuery] bool? isActive = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var query = _db.Vendors
|
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
|
.Where(v => v.IsDeleted == null || v.IsDeleted == false);
|
|
|
|
|
|
|
|
|
|
|
|
if (isActive.HasValue)
|
|
|
|
|
|
query = query.Where(v => v.IsActive == isActive.Value);
|
|
|
|
|
|
|
|
|
|
|
|
var vendors = await query
|
|
|
|
|
|
.OrderByDescending(v => v.Id)
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var companies = vendors
|
|
|
|
|
|
.Where(v => !string.IsNullOrWhiteSpace(v.CompanyName))
|
|
|
|
|
|
.GroupBy(v => v.CompanyName!.Trim(), StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
|
.Select(group => group.First())
|
|
|
|
|
|
.OrderBy(v => v.CompanyName)
|
|
|
|
|
|
.Select(v => new
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = v.CompanyName,
|
2026-07-23 17:02:40 -03:00
|
|
|
|
v.CompanyId,
|
2026-07-23 12:55:47 -03:00
|
|
|
|
v.CompanyPhone,
|
|
|
|
|
|
v.Email,
|
2026-07-23 17:02:40 -03:00
|
|
|
|
v.PreferredContact,
|
2026-07-23 12:55:47 -03:00
|
|
|
|
v.Address,
|
|
|
|
|
|
v.City,
|
|
|
|
|
|
v.State,
|
|
|
|
|
|
Zip = v.Zip,
|
|
|
|
|
|
v.GoogleMapsUrl
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var trades = vendors
|
|
|
|
|
|
.SelectMany(v => (v.TradeSpecialties ?? "")
|
|
|
|
|
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
|
|
|
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
|
.OrderBy(value => value);
|
|
|
|
|
|
|
|
|
|
|
|
var locations = vendors
|
|
|
|
|
|
.Where(v => !string.IsNullOrWhiteSpace(v.City) || !string.IsNullOrWhiteSpace(v.State))
|
|
|
|
|
|
.Select(v => new { v.City, v.State, Label = string.Join(", ", new[] { v.City, v.State }.Where(value => !string.IsNullOrWhiteSpace(value))) })
|
|
|
|
|
|
.DistinctBy(location => location.Label, StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
|
.OrderBy(location => location.Label);
|
|
|
|
|
|
|
|
|
|
|
|
return Ok(new
|
|
|
|
|
|
{
|
|
|
|
|
|
Companies = companies,
|
|
|
|
|
|
Trades = trades,
|
|
|
|
|
|
Locations = locations,
|
|
|
|
|
|
JobBuckets = new[]
|
|
|
|
|
|
{
|
|
|
|
|
|
new { Id = "under-50", Label = "Under 50" },
|
|
|
|
|
|
new { Id = "50-99", Label = "50–99" },
|
|
|
|
|
|
new { Id = "100-149", Label = "100–149" },
|
|
|
|
|
|
new { Id = "150-plus", Label = "150+" }
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 12:01:53 -04:00
|
|
|
|
[HttpGet("{id:int}/portal-token")]
|
|
|
|
|
|
public async Task<IActionResult> GetPortalToken(int id)
|
|
|
|
|
|
{
|
|
|
|
|
|
var vendor = await _db.Vendors.FindAsync(id);
|
|
|
|
|
|
if (vendor == null) return NotFound();
|
|
|
|
|
|
|
|
|
|
|
|
var token = await _vendorTokens.GetOrCreateActiveTokenAsync(id);
|
|
|
|
|
|
return Ok(BuildPortalTokenResponse(token));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[HttpPost("{id:int}/portal-token/rotate")]
|
|
|
|
|
|
public async Task<IActionResult> RotatePortalToken(int id)
|
|
|
|
|
|
{
|
|
|
|
|
|
var vendor = await _db.Vendors.FindAsync(id);
|
|
|
|
|
|
if (vendor == null) return NotFound();
|
|
|
|
|
|
|
|
|
|
|
|
var token = await _vendorTokens.RotateAsync(id);
|
|
|
|
|
|
return Ok(BuildPortalTokenResponse(token));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[HttpPost("{id:int}/portal-token/revoke")]
|
|
|
|
|
|
public async Task<IActionResult> RevokePortalToken(int id)
|
|
|
|
|
|
{
|
|
|
|
|
|
var vendor = await _db.Vendors.FindAsync(id);
|
|
|
|
|
|
if (vendor == null) return NotFound();
|
|
|
|
|
|
|
|
|
|
|
|
await _vendorTokens.RevokeAllAsync(id);
|
|
|
|
|
|
return Ok(new { revoked = true });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private object BuildPortalTokenResponse(VendorAccessToken token)
|
|
|
|
|
|
{
|
|
|
|
|
|
var frontendBase = _config.GetValue<string>("FrontendBaseUrl")?.TrimEnd('/') ?? "";
|
|
|
|
|
|
return new
|
|
|
|
|
|
{
|
|
|
|
|
|
token.Token,
|
|
|
|
|
|
token.IssuedAt,
|
|
|
|
|
|
token.ExpiresAt,
|
|
|
|
|
|
token.LastUsedAt,
|
|
|
|
|
|
PortalUrl = $"{frontendBase}/v/{token.Token}/dashboard"
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
Add vendor dispatch workflow backend
- Create Vendor model with company info, trade specialties, active flag
- Create Dispatch model (doubles as Vendor PO) with PO number, NTE, status, reply-to address
- VendorController: CRUD, paginated list, dropdown endpoint with trade filtering
- DispatchToVendor endpoint: multi-vendor dispatch, auto-generated PO numbers (VPO-00001),
HTML email with full WO details via SendGrid, reply-to wo-{number}@int.seahaven.com
- GetDispatches endpoint for listing dispatches by WO
- Include dispatches in GetWorkorderById response
- SendMessage.SendDispatchEmail with reply-to support
- Audit log entry for each dispatch
2026-04-17 11:37:35 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|