mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 10:43:13 +00:00
- VendorAccessToken model + unique-index migration; TokenLifetimeDays config - VendorPortalTokenService: CSPRNG token generation, rotation, revocation - VendorPortalController: public portal API guarded by X-Vendor-Token header; dispatches list/detail, accept, vendor status transitions, cancel request, checklist updates, signoffs (vendor + customer), comments with dispatcher attribution via AspNetUsers join - VendorController: portal-token admin endpoints (get / rotate / revoke) - WorkOrderController: dispatch email now uses vendor portal URL and HTML-encodes user fields; AddDispatchComment now stores CommentType='dispatcher' with the SHOC user's name so portal can attribute the author - DispatchPublicController: deprecated per-dispatch GET accept flow returns a static 'link no longer active' page (no state mutation)
254 lines
8.8 KiB
C#
254 lines
8.8 KiB
C#
using Api.SeaHavenIndustries.DTOs;
|
|
using Data.SeaHavenIndustries;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Api.SeaHavenIndustries.Controllers
|
|
{
|
|
[Authorize]
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
[Route("api/vendors")]
|
|
public class VendorController : Controller
|
|
{
|
|
private readonly ApplicationDbContext _db;
|
|
private readonly Helper.ZipCodeDistance _zipDistance;
|
|
private readonly Helper.VendorPortalTokenService _vendorTokens;
|
|
private readonly IConfiguration _config;
|
|
|
|
public VendorController(ApplicationDbContext db, Helper.ZipCodeDistance zipDistance, Helper.VendorPortalTokenService vendorTokens, IConfiguration config)
|
|
{
|
|
_db = db;
|
|
_zipDistance = zipDistance;
|
|
_vendorTokens = vendorTokens;
|
|
_config = config;
|
|
}
|
|
|
|
[HttpGet("GetVendorList")]
|
|
public IActionResult GetVendorList(string? search = "", int page = 1, int pageSize = 10)
|
|
{
|
|
var query = _db.Vendors.Where(v => v.IsActive);
|
|
|
|
if (!string.IsNullOrWhiteSpace(search))
|
|
{
|
|
var s = search.ToLower();
|
|
query = query.Where(v =>
|
|
(v.CompanyName ?? "").ToLower().Contains(s) ||
|
|
(v.ContactName ?? "").ToLower().Contains(s) ||
|
|
(v.Email ?? "").ToLower().Contains(s) ||
|
|
(v.TradeSpecialties ?? "").ToLower().Contains(s));
|
|
}
|
|
|
|
var totalCount = query.Count();
|
|
|
|
var data = query.OrderByDescending(v => v.Id)
|
|
.Skip((page - 1) * pageSize)
|
|
.Take(pageSize)
|
|
.Select(v => new
|
|
{
|
|
v.Id,
|
|
v.CompanyName,
|
|
v.ContactName,
|
|
v.Email,
|
|
v.Phone,
|
|
v.TradeSpecialties,
|
|
v.IsActive
|
|
})
|
|
.ToList();
|
|
|
|
return Ok(new Pagination_DTO
|
|
{
|
|
Data = data,
|
|
PageNumber = page,
|
|
PageSize = pageSize,
|
|
TotalCount = totalCount,
|
|
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
|
|
});
|
|
}
|
|
|
|
[HttpGet("{id}")]
|
|
public async Task<IActionResult> GetVendorById(int id)
|
|
{
|
|
var vendor = await _db.Vendors.Where(v => v.Id == id).Select(v => new
|
|
{
|
|
v.Id,
|
|
v.CompanyName,
|
|
v.ContactName,
|
|
v.Email,
|
|
v.Phone,
|
|
v.Address,
|
|
v.City,
|
|
v.State,
|
|
v.Zip,
|
|
v.TradeSpecialties,
|
|
v.IsActive
|
|
}).FirstOrDefaultAsync();
|
|
|
|
if (vendor == null)
|
|
return NotFound(new Response { Status = "Error", Message = "Vendor not found" });
|
|
|
|
return Ok(vendor);
|
|
}
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> Create([FromBody] Vendor_DTO model)
|
|
{
|
|
try
|
|
{
|
|
var vendor = new Vendor
|
|
{
|
|
CompanyName = model.CompanyName,
|
|
ContactName = model.ContactName,
|
|
Email = model.Email,
|
|
Phone = model.Phone,
|
|
Address = model.Address,
|
|
City = model.City,
|
|
State = model.State,
|
|
Zip = model.Zip,
|
|
TradeSpecialties = model.TradeSpecialties,
|
|
IsActive = model.IsActive
|
|
};
|
|
_db.Vendors.Add(vendor);
|
|
await _db.SaveChangesAsync();
|
|
return Ok(new DataResponse { Message = "Vendor Created", Status = "200" });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
|
}
|
|
}
|
|
|
|
[HttpPut("{id}")]
|
|
public async Task<IActionResult> Update(int id, [FromBody] EditVendor_DTO model)
|
|
{
|
|
try
|
|
{
|
|
var exist = await _db.Vendors.FindAsync(id);
|
|
if (exist == null)
|
|
return BadRequest(new Response { Status = "Error", Message = "Vendor not found" });
|
|
|
|
exist.CompanyName = model.CompanyName;
|
|
exist.ContactName = model.ContactName;
|
|
exist.Email = model.Email;
|
|
exist.Phone = model.Phone;
|
|
exist.Address = model.Address;
|
|
exist.City = model.City;
|
|
exist.State = model.State;
|
|
exist.Zip = model.Zip;
|
|
exist.TradeSpecialties = model.TradeSpecialties;
|
|
exist.IsActive = model.IsActive;
|
|
|
|
await _db.SaveChangesAsync();
|
|
return Ok(new DataResponse { Message = "Vendor Updated", Status = "200" });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
|
}
|
|
}
|
|
|
|
[HttpDelete("{id}")]
|
|
public async Task<IActionResult> Delete(int id)
|
|
{
|
|
var exist = await _db.Vendors.FindAsync(id);
|
|
if (exist == null)
|
|
return BadRequest(new Response { Status = "Error", Message = "Vendor not found" });
|
|
|
|
_db.Vendors.Remove(exist);
|
|
await _db.SaveChangesAsync();
|
|
return Ok(new DataResponse { Message = "Vendor Deleted", Status = "200" });
|
|
}
|
|
|
|
[HttpGet("Dropdown")]
|
|
public async Task<IActionResult> GetDropdown([FromQuery] string? trade = null, [FromQuery] string? siteZip = null)
|
|
{
|
|
var query = _db.Vendors.Where(v => v.IsActive);
|
|
|
|
var vendors = await query.OrderBy(v => v.CompanyName)
|
|
.Select(v => new
|
|
{
|
|
v.Id,
|
|
v.CompanyName,
|
|
v.TradeSpecialties,
|
|
v.Address,
|
|
v.City,
|
|
v.State,
|
|
v.Zip
|
|
})
|
|
.ToListAsync();
|
|
|
|
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,
|
|
v.TradeSpecialties,
|
|
address = string.Join(", ", addr),
|
|
distanceMiles = distance.HasValue ? Math.Round(distance.Value, 1) : (double?)null
|
|
};
|
|
}).ToList();
|
|
|
|
if (!string.IsNullOrWhiteSpace(trade))
|
|
{
|
|
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();
|
|
}
|
|
|
|
return Ok(result);
|
|
}
|
|
|
|
[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"
|
|
};
|
|
}
|
|
}
|
|
}
|