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 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 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 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 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 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 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 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 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 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 ResolveVendorAsync() { if (!Request.Headers.TryGetValue(TokenHeader, out var values)) return null; var token = values.ToString(); return await _tokens.ResolveVendorAsync(token); } private Task 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; } } } }