Add dispatch sign-offs with signature capture

- DispatchSignoff model (DispatchId, SignoffType, Name, Signature base64, SignatureMethod, SignedAt)
- AddDispatchSignoff endpoint — one per type per dispatch, validates no duplicate
- GetDispatchById includes signoffs in response
- Migration for DispatchSignoffs table
This commit is contained in:
Adam Moussa 2026-04-17 15:51:20 -04:00
parent c482fa0558
commit 81472bf729
8 changed files with 3001 additions and 0 deletions

View file

@ -1259,6 +1259,16 @@ namespace Api.SeaHavenIndustries.Controllers
ci.SortOrder,
ci.WorkOrderId
}).ToList(),
signoffs = _db.DispatchSignoffs
.Where(s => s.DispatchId == x.Id)
.Select(s => new {
s.Id,
s.SignoffType,
s.Name,
s.Signature,
s.SignatureMethod,
s.SignedAt
}).ToList(),
comments = _db.Comments
.Where(c => c.DispatchId == x.Id)
.OrderBy(c => c.CreatedDate)
@ -1376,6 +1386,40 @@ namespace Api.SeaHavenIndustries.Controllers
return match.Success ? match.Groups[1].Value : null;
}
[HttpPost("AddDispatchSignoff")]
public async Task<IActionResult> AddDispatchSignoff([FromBody] DispatchSignoff_DTO model)
{
try
{
var dispatch = await _db.Dispatches.FindAsync(model.DispatchId);
if (dispatch == null)
return BadRequest(new Response { Status = "Error", Message = "Dispatch not found" });
var existing = await _db.DispatchSignoffs
.AnyAsync(s => s.DispatchId == model.DispatchId && s.SignoffType == model.SignoffType);
if (existing)
return BadRequest(new Response { Status = "Error", Message = $"{model.SignoffType} sign-off already exists" });
var signoff = new DispatchSignoff
{
DispatchId = model.DispatchId,
SignoffType = model.SignoffType,
Name = model.Name,
Signature = model.Signature,
SignatureMethod = model.SignatureMethod,
SignedAt = DateTime.UtcNow
};
_db.DispatchSignoffs.Add(signoff);
await _db.SaveChangesAsync();
return Ok(new { signoff.Id, signoff.SignoffType, signoff.Name, signoff.SignedAt, signoff.SignatureMethod });
}
catch (Exception ex)
{
return BadRequest(new Response { Status = "Error", Message = ex.Message });
}
}
[HttpPost("UpdateChecklistItem")]
public async Task<IActionResult> UpdateChecklistItem([FromBody] ChecklistItemUpdate_DTO model)
{

View file

@ -0,0 +1,11 @@
namespace Api.SeaHavenIndustries.DTOs
{
public class DispatchSignoff_DTO
{
public int DispatchId { get; set; }
public string? SignoffType { get; set; }
public string? Name { get; set; }
public string? Signature { get; set; }
public string? SignatureMethod { get; set; }
}
}

View file

@ -43,6 +43,7 @@ namespace Data.SeaHavenIndustries
public DbSet<Dispatch> Dispatches { get; set; }
public DbSet<DispatchWorkOrder> DispatchWorkOrders { get; set; }
public DbSet<DispatchChecklistItem> DispatchChecklistItems { get; set; }
public DbSet<DispatchSignoff> DispatchSignoffs { get; set; }
public DbSet<TaskListTemplate> TaskListTemplates { get; set; }
public DbSet<TaskListTemplateItem> TaskListTemplateItems { get; set; }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,58 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class AddDispatchSignoffs : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DispatchSignoffs",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DispatchId = table.Column<int>(type: "int", nullable: false),
SignoffType = table.Column<string>(type: "nvarchar(max)", nullable: true),
Name = table.Column<string>(type: "nvarchar(max)", nullable: true),
Signature = table.Column<string>(type: "nvarchar(max)", nullable: true),
SignatureMethod = table.Column<string>(type: "nvarchar(max)", nullable: true),
SignedAt = 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_DispatchSignoffs", x => x.Id);
table.ForeignKey(
name: "FK_DispatchSignoffs_Dispatches_DispatchId",
column: x => x.DispatchId,
principalTable: "Dispatches",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_DispatchSignoffs_DispatchId",
table: "DispatchSignoffs",
column: "DispatchId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DispatchSignoffs");
}
}
}

View file

@ -771,6 +771,60 @@ namespace Data.SeaHavenIndustries.Migrations
b.ToTable("DispatchChecklistItems");
});
modelBuilder.Entity("Data.SeaHavenIndustries.DispatchSignoff", 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<int>("DispatchId")
.HasColumnType("int");
b.Property<bool?>("IsDeleted")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2");
b.Property<int?>("LastModifierUserId")
.HasColumnType("int");
b.Property<string>("Name")
.HasColumnType("nvarchar(max)");
b.Property<string>("Signature")
.HasColumnType("nvarchar(max)");
b.Property<string>("SignatureMethod")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("SignedAt")
.HasColumnType("datetime2");
b.Property<string>("SignoffType")
.HasColumnType("nvarchar(max)");
b.Property<string>("createdby")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("DispatchId");
b.ToTable("DispatchSignoffs");
});
modelBuilder.Entity("Data.SeaHavenIndustries.DispatchWorkOrder", b =>
{
b.Property<int>("Id")
@ -2294,6 +2348,17 @@ namespace Data.SeaHavenIndustries.Migrations
b.Navigation("WorkOrder");
});
modelBuilder.Entity("Data.SeaHavenIndustries.DispatchSignoff", b =>
{
b.HasOne("Data.SeaHavenIndustries.Dispatch", "Dispatch")
.WithMany("Signoffs")
.HasForeignKey("DispatchId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Dispatch");
});
modelBuilder.Entity("Data.SeaHavenIndustries.DispatchWorkOrder", b =>
{
b.HasOne("Data.SeaHavenIndustries.Dispatch", "Dispatch")
@ -2670,6 +2735,8 @@ namespace Data.SeaHavenIndustries.Migrations
b.Navigation("Comments");
b.Navigation("DispatchWorkOrders");
b.Navigation("Signoffs");
});
modelBuilder.Entity("Data.SeaHavenIndustries.Employee", b =>

View file

@ -27,5 +27,6 @@ namespace Data.SeaHavenIndustries
public ICollection<Comments>? Comments { get; set; }
public ICollection<DispatchWorkOrder>? DispatchWorkOrders { get; set; }
public ICollection<DispatchChecklistItem>? ChecklistItems { get; set; }
public ICollection<DispatchSignoff>? Signoffs { get; set; }
}
}

View file

@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Data.SeaHavenIndustries
{
public class DispatchSignoff : FullAuditEntity
{
public int DispatchId { get; set; }
[ForeignKey(nameof(DispatchId))]
public virtual Dispatch? Dispatch { get; set; }
public string? SignoffType { get; set; }
public string? Name { get; set; }
public string? Signature { get; set; }
public string? SignatureMethod { get; set; }
public DateTime? SignedAt { get; set; }
}
}