Add dispatcher verification as final dispatch gate

- VerifiedBy and VerifiedAt fields on Dispatch model
- VerifyDispatch endpoint validates all checklist items complete + both signoffs present
- Returns missing items list if validation fails
- Sets status to Verified, logs to audit trail
- GetDispatchById includes verifiedBy and verifiedAt
- Migration for new fields
This commit is contained in:
Adam Moussa 2026-04-17 16:03:34 -04:00
parent 81472bf729
commit 575e8ab6fc
5 changed files with 2919 additions and 0 deletions

View file

@ -1235,6 +1235,8 @@ namespace Api.SeaHavenIndustries.Controllers
x.EmailSent,
x.WorkOrderId,
x.AcknowledgedAt,
x.VerifiedBy,
x.VerifiedAt,
woNumber = x.WorkOrder != null ? x.WorkOrder.InternalWONumber : "",
woTitle = x.WorkOrder != null ? x.WorkOrder.WorkerOrderTitle : "",
vendorName = x.Vendor != null ? x.Vendor.CompanyName : "",
@ -1386,6 +1388,67 @@ namespace Api.SeaHavenIndustries.Controllers
return match.Success ? match.Groups[1].Value : null;
}
[HttpPost("VerifyDispatch")]
public async Task<IActionResult> VerifyDispatch(int dispatchId)
{
try
{
var dispatch = await _db.Dispatches.FindAsync(dispatchId);
if (dispatch == null)
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
var incompleteItems = await _db.DispatchChecklistItems
.Where(c => c.DispatchId == dispatchId && !c.IsCompleted)
.CountAsync();
var signoffCount = await _db.DispatchSignoffs
.Where(s => s.DispatchId == dispatchId)
.CountAsync();
var missing = new List<string>();
if (incompleteItems > 0)
missing.Add($"{incompleteItems} checklist item(s) incomplete");
if (signoffCount < 2)
{
var existingTypes = await _db.DispatchSignoffs
.Where(s => s.DispatchId == dispatchId)
.Select(s => s.SignoffType)
.ToListAsync();
if (!existingTypes.Contains("Customer")) missing.Add("Missing Customer sign-off");
if (!existingTypes.Contains("Vendor")) missing.Add("Missing Vendor sign-off");
}
if (missing.Count > 0)
return BadRequest(new { status = "Error", message = "Cannot verify", missing });
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var user = userId != null ? await _db.Users.FindAsync(userId) : null;
var userName = user != null ? (user.FirstName + " " + user.LastName).Trim() : "";
dispatch.Status = "Verified";
dispatch.VerifiedBy = userName;
dispatch.VerifiedAt = DateTime.UtcNow;
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
{
WorkOrderId = dispatch.WorkOrderId ?? 0,
UserId = userId,
FieldName = $"Dispatch {dispatch.DispatchNumber}",
OldValue = dispatch.Status ?? "",
NewValue = "Verified",
Action = "dispatch_verified",
CreatedAt = DateTime.UtcNow
});
await _db.SaveChangesAsync();
return Ok(new { message = "Dispatch verified", verifiedBy = userName, verifiedAt = dispatch.VerifiedAt });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPost("AddDispatchSignoff")]
public async Task<IActionResult> AddDispatchSignoff([FromBody] DispatchSignoff_DTO model)
{

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,39 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class AddDispatchVerification : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "VerifiedAt",
table: "Dispatches",
type: "datetime2",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "VerifiedBy",
table: "Dispatches",
type: "nvarchar(max)",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "VerifiedAt",
table: "Dispatches");
migrationBuilder.DropColumn(
name: "VerifiedBy",
table: "Dispatches");
}
}
}

View file

@ -697,6 +697,12 @@ namespace Data.SeaHavenIndustries.Migrations
b.Property<int>("VendorId")
.HasColumnType("int");
b.Property<DateTime?>("VerifiedAt")
.HasColumnType("datetime2");
b.Property<string>("VerifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<int?>("WorkOrderId")
.HasColumnType("int");

View file

@ -24,6 +24,8 @@ namespace Data.SeaHavenIndustries
public bool EmailSent { get; set; }
public string? ReplyToAddress { get; set; }
public string? AcceptToken { get; set; }
public string? VerifiedBy { get; set; }
public DateTime? VerifiedAt { get; set; }
public ICollection<Comments>? Comments { get; set; }
public ICollection<DispatchWorkOrder>? DispatchWorkOrders { get; set; }
public ICollection<DispatchChecklistItem>? ChecklistItems { get; set; }