mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 13:03:12 +00:00
Merge feature/vendor-portal into dev
This commit is contained in:
commit
b36ccd2758
12 changed files with 3713 additions and 76 deletions
|
|
@ -1,6 +1,4 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Api.SeaHavenIndustries.Controllers
|
||||
{
|
||||
|
|
@ -8,78 +6,34 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
[Route("api/dispatch")]
|
||||
public class DispatchPublicController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContext _db;
|
||||
|
||||
public DispatchPublicController(ApplicationDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[HttpGet("accept/{token}")]
|
||||
public async Task<IActionResult> Accept(string token)
|
||||
public IActionResult Accept(string token)
|
||||
{
|
||||
var dispatch = await _db.Dispatches
|
||||
.Include(d => d.Vendor)
|
||||
.FirstOrDefaultAsync(d => d.AcceptToken == token);
|
||||
|
||||
if (dispatch == null)
|
||||
{
|
||||
return Content(BuildHtml("Dispatch Not Found", "This dispatch link is invalid or has expired.", "#dc3545"), "text/html");
|
||||
}
|
||||
|
||||
if (dispatch.Status != "Sent")
|
||||
{
|
||||
var alreadyMsg = dispatch.AcknowledgedAt.HasValue
|
||||
? $"This dispatch was already accepted on {dispatch.AcknowledgedAt.Value:MMMM dd, yyyy 'at' h:mm tt} UTC."
|
||||
: "This dispatch has already been processed.";
|
||||
return Content(BuildHtml("Already Accepted", alreadyMsg, "#ffc107"), "text/html");
|
||||
}
|
||||
|
||||
dispatch.Status = "Acknowledged";
|
||||
dispatch.AcknowledgedAt = DateTime.UtcNow;
|
||||
|
||||
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
|
||||
{
|
||||
WorkOrderId = dispatch.WorkOrderId ?? 0,
|
||||
FieldName = $"Dispatch {dispatch.DispatchNumber} Status",
|
||||
OldValue = "Sent",
|
||||
NewValue = "Acknowledged",
|
||||
Action = "vendor_accept",
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
var vendorName = dispatch.Vendor?.CompanyName ?? "Vendor";
|
||||
return Content(BuildHtml(
|
||||
"Dispatch Accepted",
|
||||
$"<strong>{vendorName}</strong> has accepted dispatch <strong>{dispatch.DispatchNumber}</strong>.<br/><br/>Thank you for confirming. The dispatcher has been notified.",
|
||||
"#28a745"
|
||||
), "text/html");
|
||||
return Content(BuildHtml(), "text/html");
|
||||
}
|
||||
|
||||
private static string BuildHtml(string title, string message, string color)
|
||||
private static string BuildHtml()
|
||||
{
|
||||
return $@"<!DOCTYPE html>
|
||||
return @"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'/>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1'/>
|
||||
<title>{title} - Sea Haven Industries</title>
|
||||
<title>Link Expired - Sea Haven Industries</title>
|
||||
<style>
|
||||
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #f5f5f5; }}
|
||||
.card {{ background: white; border-radius: 12px; padding: 40px; max-width: 480px; text-align: center; box-shadow: 0 4px 20px rgba(0,0,0,0.1); }}
|
||||
.icon {{ width: 60px; height: 60px; border-radius: 50%; background: {color}; color: white; font-size: 28px; display: flex; align-items: center; justify-content: center; margin: 0 auto 20px; }}
|
||||
h1 {{ font-size: 22px; color: #333; margin: 0 0 12px; }}
|
||||
p {{ font-size: 15px; color: #666; line-height: 1.6; margin: 0; }}
|
||||
.footer {{ margin-top: 30px; font-size: 12px; color: #999; }}
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #f5f5f5; }
|
||||
.card { background: white; border-radius: 12px; padding: 40px; max-width: 480px; text-align: center; box-shadow: 0 4px 20px rgba(0,0,0,0.1); }
|
||||
.icon { width: 60px; height: 60px; border-radius: 50%; background: #ffc107; color: white; font-size: 28px; display: flex; align-items: center; justify-content: center; margin: 0 auto 20px; }
|
||||
h1 { font-size: 22px; color: #333; margin: 0 0 12px; }
|
||||
p { font-size: 15px; color: #666; line-height: 1.6; margin: 0; }
|
||||
.footer { margin-top: 30px; font-size: 12px; color: #999; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class='card'>
|
||||
<div class='icon'>✓</div>
|
||||
<h1>{title}</h1>
|
||||
<p>{message}</p>
|
||||
<div class='icon'>!</div>
|
||||
<h1>Link No Longer Active</h1>
|
||||
<p>This dispatch accept link has been retired. Please use the vendor portal link in your most recent dispatch email, or contact your dispatcher for a new link.</p>
|
||||
<div class='footer'>Sea Haven Industries</div>
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -14,11 +14,15 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
{
|
||||
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)
|
||||
public VendorController(ApplicationDbContext db, Helper.ZipCodeDistance zipDistance, Helper.VendorPortalTokenService vendorTokens, IConfiguration config)
|
||||
{
|
||||
_db = db;
|
||||
_zipDistance = zipDistance;
|
||||
_vendorTokens = vendorTokens;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
[HttpGet("GetVendorList")]
|
||||
|
|
@ -203,5 +207,48 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
|
||||
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"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
491
Api.SeaHavenIndustries/Controllers/VendorPortalController.cs
Normal file
491
Api.SeaHavenIndustries/Controllers/VendorPortalController.cs
Normal file
|
|
@ -0,0 +1,491 @@
|
|||
using Api.SeaHavenIndustries.Helper;
|
||||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Api.SeaHavenIndustries.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/vendor-portal")]
|
||||
[Route("api/vendorportal")]
|
||||
public class VendorPortalController : Controller
|
||||
{
|
||||
private const string TokenHeader = "X-Vendor-Token";
|
||||
|
||||
private static readonly string[] VendorViewableStatuses =
|
||||
new[] { "Sent", "Acknowledged", "In Progress", "Completed", "Verified", "Cancelled" };
|
||||
|
||||
private readonly ApplicationDbContext _db;
|
||||
private readonly VendorPortalTokenService _tokens;
|
||||
|
||||
public VendorPortalController(ApplicationDbContext db, VendorPortalTokenService tokens)
|
||||
{
|
||||
_db = db;
|
||||
_tokens = tokens;
|
||||
}
|
||||
|
||||
[HttpGet("session")]
|
||||
public async Task<IActionResult> Session()
|
||||
{
|
||||
var vendor = await ResolveVendorAsync();
|
||||
if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" });
|
||||
|
||||
return Ok(new DataResponse
|
||||
{
|
||||
Status = "Success",
|
||||
Data = new
|
||||
{
|
||||
vendor.Id,
|
||||
vendor.CompanyName,
|
||||
vendor.ContactName,
|
||||
vendor.Email,
|
||||
vendor.Phone
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("dispatches")]
|
||||
public async Task<IActionResult> ListDispatches([FromQuery] string? status = null)
|
||||
{
|
||||
var vendor = await ResolveVendorAsync();
|
||||
if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" });
|
||||
|
||||
var query = _db.Dispatches
|
||||
.Include(d => d.WorkOrder).ThenInclude(w => w!.Locations)
|
||||
.Where(d => d.VendorId == vendor.Id && (d.IsDeleted == null || d.IsDeleted == false));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(status))
|
||||
{
|
||||
query = query.Where(d => d.Status == status);
|
||||
}
|
||||
|
||||
var dispatches = await query
|
||||
.OrderByDescending(d => d.DispatchedAt ?? d.CreatedDate)
|
||||
.Select(d => new
|
||||
{
|
||||
d.Id,
|
||||
d.DispatchNumber,
|
||||
d.PONumber,
|
||||
d.Status,
|
||||
d.NTEAmount,
|
||||
d.ScheduledDate,
|
||||
d.CompletedDate,
|
||||
d.DispatchedAt,
|
||||
d.AcknowledgedAt,
|
||||
WorkOrderTitle = d.WorkOrder!.WorkerOrderTitle,
|
||||
InternalWONumber = d.WorkOrder!.InternalWONumber,
|
||||
LocationName = d.WorkOrder!.Locations != null ? d.WorkOrder!.Locations.Name : null,
|
||||
LocationCity = d.WorkOrder!.Locations != null ? d.WorkOrder!.Locations.City : null,
|
||||
LocationState = d.WorkOrder!.Locations != null ? d.WorkOrder!.Locations.State : null
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(new DataResponse { Status = "Success", Data = dispatches });
|
||||
}
|
||||
|
||||
[HttpGet("dispatches/{id:int}")]
|
||||
public async Task<IActionResult> GetDispatch(int id)
|
||||
{
|
||||
var vendor = await ResolveVendorAsync();
|
||||
if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" });
|
||||
|
||||
var dispatch = await LoadVendorDispatchAsync(id, vendor.Id);
|
||||
if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
|
||||
|
||||
var checklist = await _db.DispatchChecklistItems
|
||||
.Where(c => c.DispatchId == id && (c.IsDeleted == null || c.IsDeleted == false))
|
||||
.OrderBy(c => c.SortOrder)
|
||||
.Select(c => new { c.Id, c.ItemText, c.IsCompleted, c.CompletedBy, c.CompletedAt })
|
||||
.ToListAsync();
|
||||
|
||||
var signoffs = await _db.DispatchSignoffs
|
||||
.Where(s => s.DispatchId == id && (s.IsDeleted == null || s.IsDeleted == false))
|
||||
.Select(s => new { s.Id, s.SignoffType, s.Name, s.SignatureMethod, s.SignedAt })
|
||||
.ToListAsync();
|
||||
|
||||
var rawComments = await (from c in _db.Comments
|
||||
where c.DispatchId == id
|
||||
&& c.CommentType != "internal"
|
||||
&& (c.IsDeleted == null || c.IsDeleted == false)
|
||||
join u in _db.Users on c.UserId equals u.Id into users
|
||||
from u in users.DefaultIfEmpty()
|
||||
orderby c.CreatedDate
|
||||
select new
|
||||
{
|
||||
c.Id,
|
||||
c.Commenttext,
|
||||
c.Commenter,
|
||||
c.CommentType,
|
||||
c.CreatedDate,
|
||||
c.UserId,
|
||||
UserFirstName = u != null ? u.FirstName : null,
|
||||
UserLastName = u != null ? u.LastName : null
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var comments = rawComments.Select(c =>
|
||||
{
|
||||
var hasShocUser = !string.IsNullOrWhiteSpace(c.UserId);
|
||||
var userName = string.Join(" ", new[] { c.UserFirstName, c.UserLastName }
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s))).Trim();
|
||||
|
||||
string resolvedType;
|
||||
string resolvedCommenter;
|
||||
if (hasShocUser)
|
||||
{
|
||||
resolvedType = "dispatcher";
|
||||
resolvedCommenter = !string.IsNullOrWhiteSpace(userName) ? userName
|
||||
: !string.IsNullOrWhiteSpace(c.Commenter) ? c.Commenter!
|
||||
: "Dispatcher";
|
||||
}
|
||||
else if (c.CommentType == "customer")
|
||||
{
|
||||
resolvedType = "customer";
|
||||
resolvedCommenter = !string.IsNullOrWhiteSpace(c.Commenter) ? c.Commenter! : "Customer";
|
||||
}
|
||||
else
|
||||
{
|
||||
resolvedType = "vendor";
|
||||
resolvedCommenter = !string.IsNullOrWhiteSpace(c.Commenter) ? c.Commenter! : vendor.CompanyName ?? "Vendor";
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
c.Id,
|
||||
c.Commenttext,
|
||||
Commenter = resolvedCommenter,
|
||||
CommentType = resolvedType,
|
||||
c.CreatedDate
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return Ok(new DataResponse
|
||||
{
|
||||
Status = "Success",
|
||||
Data = new
|
||||
{
|
||||
dispatch.Id,
|
||||
dispatch.DispatchNumber,
|
||||
dispatch.PONumber,
|
||||
dispatch.Status,
|
||||
dispatch.NTEAmount,
|
||||
dispatch.Description,
|
||||
dispatch.ScheduledDate,
|
||||
dispatch.CompletedDate,
|
||||
dispatch.DispatchedAt,
|
||||
dispatch.AcknowledgedAt,
|
||||
WorkOrder = dispatch.WorkOrder == null ? null : new
|
||||
{
|
||||
dispatch.WorkOrder.Id,
|
||||
dispatch.WorkOrder.InternalWONumber,
|
||||
dispatch.WorkOrder.WorkerOrderTitle,
|
||||
dispatch.WorkOrder.Description,
|
||||
dispatch.WorkOrder.Priority,
|
||||
dispatch.WorkOrder.DueDate,
|
||||
dispatch.WorkOrder.Trade,
|
||||
dispatch.WorkOrder.SubTrade,
|
||||
dispatch.WorkOrder.Problem
|
||||
},
|
||||
Location = dispatch.WorkOrder?.Locations == null ? null : new
|
||||
{
|
||||
dispatch.WorkOrder.Locations.Name,
|
||||
dispatch.WorkOrder.Locations.Address,
|
||||
dispatch.WorkOrder.Locations.City,
|
||||
dispatch.WorkOrder.Locations.State,
|
||||
dispatch.WorkOrder.Locations.ZipCode
|
||||
},
|
||||
Checklist = checklist,
|
||||
Signoffs = signoffs,
|
||||
Comments = comments
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("dispatches/{id:int}/accept")]
|
||||
public async Task<IActionResult> Accept(int id)
|
||||
{
|
||||
var vendor = await ResolveVendorAsync();
|
||||
if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" });
|
||||
|
||||
var dispatch = await LoadVendorDispatchAsync(id, vendor.Id);
|
||||
if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
|
||||
|
||||
if (dispatch.Status != "Sent")
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = $"Cannot accept a dispatch with status '{dispatch.Status}'" });
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
dispatch.Status = "Acknowledged";
|
||||
dispatch.AcknowledgedAt = now;
|
||||
dispatch.LastModificationTime = now;
|
||||
|
||||
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
|
||||
{
|
||||
WorkOrderId = dispatch.WorkOrderId ?? 0,
|
||||
FieldName = $"Dispatch {dispatch.DispatchNumber} Status",
|
||||
OldValue = "Sent",
|
||||
NewValue = "Acknowledged",
|
||||
Action = "vendor_accept",
|
||||
CreatedAt = now
|
||||
});
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return Ok(new DataResponse { Status = "Success", Data = new { dispatch.Id, dispatch.Status, dispatch.AcknowledgedAt } });
|
||||
}
|
||||
|
||||
[HttpPost("dispatches/{id:int}/status")]
|
||||
public async Task<IActionResult> ChangeStatus(int id, [FromBody] ChangeStatusRequest body)
|
||||
{
|
||||
var vendor = await ResolveVendorAsync();
|
||||
if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" });
|
||||
|
||||
var dispatch = await LoadVendorDispatchAsync(id, vendor.Id);
|
||||
if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
|
||||
|
||||
var from = dispatch.Status;
|
||||
var to = body.Status;
|
||||
if (!IsAllowedVendorTransition(from, to))
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = $"Transition from '{from}' to '{to}' is not allowed" });
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
dispatch.Status = to;
|
||||
dispatch.LastModificationTime = now;
|
||||
if (to == "Completed") dispatch.CompletedDate = now;
|
||||
|
||||
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
|
||||
{
|
||||
WorkOrderId = dispatch.WorkOrderId ?? 0,
|
||||
FieldName = $"Dispatch {dispatch.DispatchNumber} Status",
|
||||
OldValue = from,
|
||||
NewValue = to,
|
||||
Action = "vendor_status_change",
|
||||
CreatedAt = now
|
||||
});
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return Ok(new DataResponse { Status = "Success", Data = new { dispatch.Id, dispatch.Status, dispatch.CompletedDate } });
|
||||
}
|
||||
|
||||
[HttpPost("dispatches/{id:int}/request-cancel")]
|
||||
public async Task<IActionResult> RequestCancel(int id, [FromBody] RequestCancelRequest body)
|
||||
{
|
||||
var vendor = await ResolveVendorAsync();
|
||||
if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" });
|
||||
|
||||
var dispatch = await LoadVendorDispatchAsync(id, vendor.Id);
|
||||
if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
|
||||
|
||||
if (dispatch.Status == "Verified" || dispatch.Status == "Cancelled")
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = $"Cannot request cancel on a '{dispatch.Status}' dispatch" });
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var reason = string.IsNullOrWhiteSpace(body?.Reason) ? "(no reason provided)" : body!.Reason!.Trim();
|
||||
|
||||
_db.Comments.Add(new Comments
|
||||
{
|
||||
DispatchId = id,
|
||||
WorkerOrderId = dispatch.WorkOrderId,
|
||||
Commenter = vendor.CompanyName,
|
||||
CommentType = "vendor",
|
||||
RecordType = "cancel_request",
|
||||
Commenttext = $"Vendor requested cancellation: {reason}",
|
||||
CreatedDate = now
|
||||
});
|
||||
|
||||
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
|
||||
{
|
||||
WorkOrderId = dispatch.WorkOrderId ?? 0,
|
||||
FieldName = $"Dispatch {dispatch.DispatchNumber}",
|
||||
OldValue = dispatch.Status,
|
||||
NewValue = "Cancel Requested",
|
||||
Action = "vendor_request_cancel",
|
||||
CreatedAt = now
|
||||
});
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return Ok(new DataResponse { Status = "Success", Message = "Cancel request sent to dispatcher" });
|
||||
}
|
||||
|
||||
[HttpPost("dispatches/{id:int}/checklist/{itemId:int}")]
|
||||
public async Task<IActionResult> UpdateChecklistItem(int id, int itemId, [FromBody] ChecklistUpdateRequest body)
|
||||
{
|
||||
var vendor = await ResolveVendorAsync();
|
||||
if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" });
|
||||
|
||||
var dispatch = await LoadVendorDispatchAsync(id, vendor.Id);
|
||||
if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
|
||||
|
||||
if (dispatch.Status == "Verified" || dispatch.Status == "Cancelled")
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = "Dispatch is locked" });
|
||||
}
|
||||
|
||||
var item = await _db.DispatchChecklistItems
|
||||
.FirstOrDefaultAsync(c => c.Id == itemId && c.DispatchId == id);
|
||||
if (item == null) return NotFound(new Response { Status = "Error", Message = "Checklist item not found" });
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
item.IsCompleted = body.IsCompleted;
|
||||
item.CompletedBy = body.IsCompleted ? vendor.CompanyName : null;
|
||||
item.CompletedAt = body.IsCompleted ? now : null;
|
||||
item.LastModificationTime = now;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return Ok(new DataResponse
|
||||
{
|
||||
Status = "Success",
|
||||
Data = new { item.Id, item.IsCompleted, item.CompletedBy, item.CompletedAt }
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("dispatches/{id:int}/signoff")]
|
||||
public async Task<IActionResult> AddSignoff(int id, [FromBody] SignoffRequest body)
|
||||
{
|
||||
var vendor = await ResolveVendorAsync();
|
||||
if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" });
|
||||
|
||||
var dispatch = await LoadVendorDispatchAsync(id, vendor.Id);
|
||||
if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
|
||||
|
||||
if (dispatch.Status != "In Progress" && dispatch.Status != "Completed")
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = "Signoffs only allowed on In Progress or Completed dispatches" });
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body?.Name) || string.IsNullOrWhiteSpace(body?.Signature))
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = "Name and signature are required" });
|
||||
}
|
||||
|
||||
var signoffType = (body?.SignoffType ?? "vendor").Trim().ToLowerInvariant();
|
||||
if (signoffType != "vendor" && signoffType != "customer")
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = "signoffType must be 'vendor' or 'customer'" });
|
||||
}
|
||||
|
||||
var existing = await _db.DispatchSignoffs
|
||||
.FirstOrDefaultAsync(s => s.DispatchId == id && s.SignoffType == signoffType);
|
||||
if (existing != null)
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = $"A {signoffType} signoff already exists for this dispatch" });
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var signoff = new DispatchSignoff
|
||||
{
|
||||
DispatchId = id,
|
||||
SignoffType = signoffType,
|
||||
Name = body!.Name,
|
||||
Signature = body.Signature,
|
||||
SignatureMethod = body.SignatureMethod ?? "drawn",
|
||||
SignedAt = now,
|
||||
CreatedDate = now
|
||||
};
|
||||
_db.DispatchSignoffs.Add(signoff);
|
||||
|
||||
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
|
||||
{
|
||||
WorkOrderId = dispatch.WorkOrderId ?? 0,
|
||||
FieldName = $"Dispatch {dispatch.DispatchNumber} Signoff",
|
||||
OldValue = null,
|
||||
NewValue = $"{(signoffType == "customer" ? "Customer" : "Vendor")}: {body.Name}",
|
||||
Action = signoffType == "customer" ? "customer_signoff" : "vendor_signoff",
|
||||
CreatedAt = now
|
||||
});
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return Ok(new DataResponse { Status = "Success", Data = new { signoff.Id, signoff.SignedAt } });
|
||||
}
|
||||
|
||||
[HttpPost("dispatches/{id:int}/comments")]
|
||||
public async Task<IActionResult> AddComment(int id, [FromBody] CommentRequest body)
|
||||
{
|
||||
var vendor = await ResolveVendorAsync();
|
||||
if (vendor == null) return Unauthorized(new Response { Status = "Error", Message = "Invalid or expired token" });
|
||||
|
||||
var dispatch = await LoadVendorDispatchAsync(id, vendor.Id);
|
||||
if (dispatch == null) return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body?.CommentText))
|
||||
{
|
||||
return BadRequest(new Response { Status = "Error", Message = "Comment cannot be empty" });
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var comment = new Comments
|
||||
{
|
||||
DispatchId = id,
|
||||
WorkerOrderId = dispatch.WorkOrderId,
|
||||
Commenter = vendor.CompanyName,
|
||||
CommentType = "vendor",
|
||||
RecordType = "comment",
|
||||
Commenttext = body!.CommentText,
|
||||
CreatedDate = now
|
||||
};
|
||||
_db.Comments.Add(comment);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(new DataResponse
|
||||
{
|
||||
Status = "Success",
|
||||
Data = new { comment.Id, comment.Commenttext, comment.Commenter, comment.CreatedDate }
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<Vendor?> ResolveVendorAsync()
|
||||
{
|
||||
if (!Request.Headers.TryGetValue(TokenHeader, out var values)) return null;
|
||||
var token = values.ToString();
|
||||
return await _tokens.ResolveVendorAsync(token);
|
||||
}
|
||||
|
||||
private Task<Dispatch?> LoadVendorDispatchAsync(int id, int vendorId)
|
||||
{
|
||||
return _db.Dispatches
|
||||
.Include(d => d.WorkOrder).ThenInclude(w => w!.Locations)
|
||||
.FirstOrDefaultAsync(d => d.Id == id
|
||||
&& d.VendorId == vendorId
|
||||
&& (d.IsDeleted == null || d.IsDeleted == false));
|
||||
}
|
||||
|
||||
private static bool IsAllowedVendorTransition(string? from, string? to)
|
||||
{
|
||||
if (from == "Acknowledged" && to == "In Progress") return true;
|
||||
if (from == "In Progress" && to == "Completed") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public class ChangeStatusRequest
|
||||
{
|
||||
public string? Status { get; set; }
|
||||
}
|
||||
|
||||
public class RequestCancelRequest
|
||||
{
|
||||
public string? Reason { get; set; }
|
||||
}
|
||||
|
||||
public class ChecklistUpdateRequest
|
||||
{
|
||||
public bool IsCompleted { get; set; }
|
||||
}
|
||||
|
||||
public class SignoffRequest
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? Signature { get; set; }
|
||||
public string? SignatureMethod { get; set; }
|
||||
public string? SignoffType { get; set; }
|
||||
}
|
||||
|
||||
public class CommentRequest
|
||||
{
|
||||
public string? CommentText { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,13 +27,17 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
private readonly IWebHostEnvironment _webHostEnvironment;
|
||||
IHttpContextAccessor _httpContext;
|
||||
private readonly Helper.SendMessage _sendMessage;
|
||||
public WorkOrderController(UserManager<ApplicationUser> userManager, ApplicationDbContext db, IWebHostEnvironment webHostEnvironment, IHttpContextAccessor httpContext, Helper.SendMessage sendMessage)
|
||||
private readonly Helper.VendorPortalTokenService _vendorTokens;
|
||||
private readonly IConfiguration _config;
|
||||
public WorkOrderController(UserManager<ApplicationUser> userManager, ApplicationDbContext db, IWebHostEnvironment webHostEnvironment, IHttpContextAccessor httpContext, Helper.SendMessage sendMessage, Helper.VendorPortalTokenService vendorTokens, IConfiguration config)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_db = db;
|
||||
_webHostEnvironment = webHostEnvironment;
|
||||
_httpContext = httpContext;
|
||||
_sendMessage = sendMessage;
|
||||
_vendorTokens = vendorTokens;
|
||||
_config = config;
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("AddWorkorder")]
|
||||
|
|
@ -1089,7 +1093,6 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
ScheduledDate = model.ScheduledDate,
|
||||
DispatchedAt = DateTime.UtcNow,
|
||||
ReplyToAddress = replyTo,
|
||||
AcceptToken = Guid.NewGuid().ToString(),
|
||||
DispatchWorkOrders = workOrders.Select(wo => new DispatchWorkOrder { WorkOrderId = wo.Id }).ToList()
|
||||
};
|
||||
|
||||
|
|
@ -1114,14 +1117,20 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
dispatch.ChecklistItems = checklistItems;
|
||||
|
||||
var woListHtml = string.Join("", workOrders.Select(wo =>
|
||||
$"<tr><td style='padding:6px 8px;border:1px solid #ddd;'>{wo.InternalWONumber}</td>" +
|
||||
$"<td style='padding:6px 8px;border:1px solid #ddd;'>{wo.WorkerOrderTitle}</td>" +
|
||||
$"<td style='padding:6px 8px;border:1px solid #ddd;'>{wo.Priority}</td>" +
|
||||
$"<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 = primaryWO.Locations?.Name ?? "";
|
||||
var locationAddr = primaryWO.Locations?.Address ?? "";
|
||||
var subject = $"[{dispatchNumber}] {(workOrders.Count > 1 ? $"{workOrders.Count} Work Orders" : primaryWO.WorkerOrderTitle)} at {locationName}";
|
||||
var locationName = System.Net.WebUtility.HtmlEncode(primaryWO.Locations?.Name ?? "");
|
||||
var locationAddr = System.Net.WebUtility.HtmlEncode(primaryWO.Locations?.Address ?? "");
|
||||
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>
|
||||
|
|
@ -1138,12 +1147,12 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
{woListHtml}
|
||||
</table>
|
||||
<h3>Description</h3>
|
||||
<p>{model.Description ?? primaryWO.Description}</p>
|
||||
<p>{descriptionHtml}</p>
|
||||
<br/>
|
||||
<div style='text-align:center;margin:20px 0;'>
|
||||
<a href='{Request.Scheme}://{Request.Host}/api/dispatch/accept/{dispatch.AcceptToken}' style='display:inline-block;padding:12px 32px;background:#28a745;color:white;text-decoration:none;border-radius:6px;font-weight:bold;font-size:16px;'>Accept Dispatch</a>
|
||||
<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;'>{senderName}<br/>Sea Haven Industries</p>
|
||||
<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);
|
||||
|
|
@ -1347,6 +1356,8 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
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
|
||||
{
|
||||
|
|
@ -1355,14 +1366,12 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
WorkerOrderId = dispatch.WorkOrderId,
|
||||
DispatchId = dispatch.Id,
|
||||
Commenttext = model.Text,
|
||||
CommentType = "vendor",
|
||||
CommentType = "dispatcher",
|
||||
Commenter = !string.IsNullOrWhiteSpace(senderName) ? senderName : null,
|
||||
};
|
||||
_db.Comments.Add(comment);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
var user = await _db.Users.FindAsync(userId);
|
||||
var senderName = user != null ? (user.FirstName + " " + user.LastName).Trim() : "";
|
||||
|
||||
if (model.SendEmail && dispatch.Vendor?.Email != null && dispatch.WorkOrder != null)
|
||||
{
|
||||
var subject = $"[WO-{dispatch.WorkOrder.InternalWONumber}-{dispatch.DispatchNumber}] {dispatch.WorkOrder.WorkerOrderTitle}";
|
||||
|
|
|
|||
92
Api.SeaHavenIndustries/Helper/VendorPortalTokenService.cs
Normal file
92
Api.SeaHavenIndustries/Helper/VendorPortalTokenService.cs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
using System.Security.Cryptography;
|
||||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Api.SeaHavenIndustries.Helper
|
||||
{
|
||||
public class VendorPortalTokenService
|
||||
{
|
||||
private readonly ApplicationDbContext _db;
|
||||
private readonly int _lifetimeDays;
|
||||
|
||||
public VendorPortalTokenService(ApplicationDbContext db, IConfiguration config)
|
||||
{
|
||||
_db = db;
|
||||
_lifetimeDays = config.GetValue<int?>("VendorPortal:TokenLifetimeDays") ?? 365;
|
||||
}
|
||||
|
||||
public async Task<VendorAccessToken> GetOrCreateActiveTokenAsync(int vendorId)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var existing = await _db.VendorAccessTokens
|
||||
.Where(t => t.VendorId == vendorId
|
||||
&& t.RevokedAt == null
|
||||
&& t.ExpiresAt > now
|
||||
&& (t.IsDeleted == null || t.IsDeleted == false))
|
||||
.OrderByDescending(t => t.IssuedAt)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (existing != null) return existing;
|
||||
|
||||
var token = new VendorAccessToken
|
||||
{
|
||||
VendorId = vendorId,
|
||||
Token = GenerateToken(),
|
||||
IssuedAt = now,
|
||||
ExpiresAt = now.AddDays(_lifetimeDays),
|
||||
CreatedDate = now
|
||||
};
|
||||
_db.VendorAccessTokens.Add(token);
|
||||
await _db.SaveChangesAsync();
|
||||
return token;
|
||||
}
|
||||
|
||||
public async Task<Vendor?> ResolveVendorAsync(string? token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token)) return null;
|
||||
var now = DateTime.UtcNow;
|
||||
var record = await _db.VendorAccessTokens
|
||||
.Include(t => t.Vendor)
|
||||
.FirstOrDefaultAsync(t => t.Token == token
|
||||
&& t.RevokedAt == null
|
||||
&& t.ExpiresAt > now
|
||||
&& (t.IsDeleted == null || t.IsDeleted == false));
|
||||
|
||||
if (record?.Vendor == null) return null;
|
||||
|
||||
record.LastUsedAt = now;
|
||||
await _db.SaveChangesAsync();
|
||||
return record.Vendor;
|
||||
}
|
||||
|
||||
public async Task<VendorAccessToken> RotateAsync(int vendorId)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var active = await _db.VendorAccessTokens
|
||||
.Where(t => t.VendorId == vendorId && t.RevokedAt == null)
|
||||
.ToListAsync();
|
||||
foreach (var t in active) t.RevokedAt = now;
|
||||
await _db.SaveChangesAsync();
|
||||
return await GetOrCreateActiveTokenAsync(vendorId);
|
||||
}
|
||||
|
||||
public async Task RevokeAllAsync(int vendorId)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var active = await _db.VendorAccessTokens
|
||||
.Where(t => t.VendorId == vendorId && t.RevokedAt == null)
|
||||
.ToListAsync();
|
||||
foreach (var t in active) t.RevokedAt = now;
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static string GenerateToken()
|
||||
{
|
||||
var bytes = RandomNumberGenerator.GetBytes(32);
|
||||
return Convert.ToBase64String(bytes)
|
||||
.Replace("+", "-")
|
||||
.Replace("/", "_")
|
||||
.TrimEnd('=');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ builder.Services.AddResponseCompression(opts =>
|
|||
builder.Services.AddCors(option =>
|
||||
option.AddDefaultPolicy(builder => builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()));
|
||||
builder.Services.AddScoped<SendMessage>();
|
||||
builder.Services.AddScoped<Api.SeaHavenIndustries.Helper.VendorPortalTokenService>();
|
||||
builder.Services.AddSingleton<Api.SeaHavenIndustries.Helper.ZipCodeDistance>(sp =>
|
||||
{
|
||||
var zipService = new Api.SeaHavenIndustries.Helper.ZipCodeDistance();
|
||||
|
|
|
|||
|
|
@ -15,5 +15,9 @@
|
|||
"ValidAudience": "http://localhost:3000",
|
||||
"ValidIssuer": "https://localhost:7195",
|
||||
"Secret": "JWTAuthenticationHIGHsecuredPasswordVVVp1OH7Xzyr"
|
||||
},
|
||||
"FrontendBaseUrl": "http://localhost:3000",
|
||||
"VendorPortal": {
|
||||
"TokenLifetimeDays": 365
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ namespace Data.SeaHavenIndustries
|
|||
{
|
||||
foreignKey.DeleteBehavior = DeleteBehavior.Restrict;
|
||||
}
|
||||
|
||||
builder.Entity<VendorAccessToken>()
|
||||
.HasIndex(t => t.Token)
|
||||
.IsUnique();
|
||||
}
|
||||
public DbSet<Category> Categories { get; set; }
|
||||
public DbSet<Locations> Locations { get; set; }
|
||||
|
|
@ -40,6 +44,7 @@ namespace Data.SeaHavenIndustries
|
|||
public DbSet<WorkOrderAuditLog> WorkOrderAuditLogs { get; set; }
|
||||
public DbSet<DropdownOption> DropdownOptions { get; set; }
|
||||
public DbSet<Vendor> Vendors { get; set; }
|
||||
public DbSet<VendorAccessToken> VendorAccessTokens { get; set; }
|
||||
public DbSet<Dispatch> Dispatches { get; set; }
|
||||
public DbSet<DispatchWorkOrder> DispatchWorkOrders { get; set; }
|
||||
public DbSet<DispatchChecklistItem> DispatchChecklistItems { get; set; }
|
||||
|
|
|
|||
2879
Data.SeaHavenIndustries/Migrations/20260420151445_AddVendorAccessToken.Designer.cs
generated
Normal file
2879
Data.SeaHavenIndustries/Migrations/20260420151445_AddVendorAccessToken.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,64 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.SeaHavenIndustries.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddVendorAccessToken : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "VendorAccessTokens",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
VendorId = table.Column<int>(type: "int", nullable: false),
|
||||
Token = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
IssuedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
ExpiresAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
RevokedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastUsedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: true),
|
||||
createdby = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
DeleterUserId = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifierUserId = table.Column<int>(type: "int", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_VendorAccessTokens", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_VendorAccessTokens_Vendors_VendorId",
|
||||
column: x => x.VendorId,
|
||||
principalTable: "Vendors",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VendorAccessTokens_Token",
|
||||
table: "VendorAccessTokens",
|
||||
column: "Token",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VendorAccessTokens_VendorId",
|
||||
table: "VendorAccessTokens",
|
||||
column: "VendorId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "VendorAccessTokens");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1788,6 +1788,65 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.ToTable("Vendors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.VendorAccessToken", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime?>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("DeleterUserId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool?>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime>("IssuedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int?>("LastModifierUserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("LastUsedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("RevokedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<int>("VendorId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("createdby")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Token")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("VendorId");
|
||||
|
||||
b.ToTable("VendorAccessTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrder", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
@ -2568,6 +2627,17 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Navigation("POC");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.VendorAccessToken", b =>
|
||||
{
|
||||
b.HasOne("Data.SeaHavenIndustries.Vendor", "Vendor")
|
||||
.WithMany()
|
||||
.HasForeignKey("VendorId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Vendor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.WorkOrder", b =>
|
||||
{
|
||||
b.HasOne("Data.SeaHavenIndustries.ApplicationUser", "AssignToUser")
|
||||
|
|
|
|||
21
Data.SeaHavenIndustries/Models/VendorAccessToken.cs
Normal file
21
Data.SeaHavenIndustries/Models/VendorAccessToken.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Data.SeaHavenIndustries
|
||||
{
|
||||
public class VendorAccessToken : FullAuditEntity
|
||||
{
|
||||
public int VendorId { get; set; }
|
||||
[ForeignKey(nameof(VendorId))]
|
||||
public virtual Vendor? Vendor { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(64)]
|
||||
public string Token { get; set; } = string.Empty;
|
||||
|
||||
public DateTime IssuedAt { get; set; }
|
||||
public DateTime ExpiresAt { get; set; }
|
||||
public DateTime? RevokedAt { get; set; }
|
||||
public DateTime? LastUsedAt { get; set; }
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue