Add dispatch checklist items with template support

- DispatchChecklistItem model (DispatchId, WorkOrderId, ItemText, IsCompleted, CompletedBy, CompletedAt)
- DispatchToVendor copies template items when TaskListTemplateId provided
- Supports custom checklist items alongside template items
- UpdateChecklistItem endpoint to toggle completion with user name
- AddChecklistItem endpoint for ad-hoc items
- GetDispatchById includes checklist items in response
- Migration for DispatchChecklistItems table
This commit is contained in:
Adam Moussa 2026-04-17 15:28:52 -04:00
parent be190836a4
commit 3a25fa8559
8 changed files with 2989 additions and 0 deletions

View file

@ -1090,6 +1090,26 @@ namespace Api.SeaHavenIndustries.Controllers
DispatchWorkOrders = workOrders.Select(wo => new DispatchWorkOrder { WorkOrderId = wo.Id }).ToList()
};
var checklistItems = new List<DispatchChecklistItem>();
if (model.TaskListTemplateId.HasValue)
{
var template = await _db.TaskListTemplates
.Include(t => t.Items)
.FirstOrDefaultAsync(t => t.Id == model.TaskListTemplateId.Value);
if (template?.Items != null)
{
checklistItems.AddRange(template.Items.OrderBy(i => i.SortOrder).Select((i, idx) =>
new DispatchChecklistItem { ItemText = i.ItemText, SortOrder = idx }));
}
}
if (model.CustomChecklistItems != null)
{
var startIdx = checklistItems.Count;
checklistItems.AddRange(model.CustomChecklistItems.Select((text, idx) =>
new DispatchChecklistItem { ItemText = text, SortOrder = startIdx + idx }));
}
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>" +
@ -1223,6 +1243,18 @@ namespace Api.SeaHavenIndustries.Controllers
woNumber = dwo.WorkOrder != null ? dwo.WorkOrder.InternalWONumber : "",
woTitle = dwo.WorkOrder != null ? dwo.WorkOrder.WorkerOrderTitle : ""
}).ToList(),
checklistItems = _db.DispatchChecklistItems
.Where(ci => ci.DispatchId == x.Id)
.OrderBy(ci => ci.SortOrder)
.Select(ci => new {
ci.Id,
ci.ItemText,
ci.IsCompleted,
ci.CompletedBy,
ci.CompletedAt,
ci.SortOrder,
ci.WorkOrderId
}).ToList(),
comments = _db.Comments
.Where(c => c.DispatchId == x.Id)
.OrderBy(c => c.CreatedDate)
@ -1340,6 +1372,41 @@ namespace Api.SeaHavenIndustries.Controllers
return match.Success ? match.Groups[1].Value : null;
}
[HttpPost("UpdateChecklistItem")]
public async Task<IActionResult> UpdateChecklistItem([FromBody] ChecklistItemUpdate_DTO model)
{
var item = await _db.DispatchChecklistItems.FindAsync(model.Id);
if (item == null)
return NotFound(new Response { Status = "Error", Message = "Checklist item not found" });
item.IsCompleted = model.IsCompleted;
item.CompletedBy = model.IsCompleted ? model.CompletedBy : null;
item.CompletedAt = model.IsCompleted ? DateTime.UtcNow : null;
await _db.SaveChangesAsync();
return Ok(new { message = "Updated", item.Id, item.IsCompleted, item.CompletedBy, item.CompletedAt });
}
[HttpPost("AddChecklistItem")]
public async Task<IActionResult> AddChecklistItem([FromBody] AddChecklistItem_DTO model)
{
var maxSort = await _db.DispatchChecklistItems
.Where(c => c.DispatchId == model.DispatchId)
.MaxAsync(c => (int?)c.SortOrder) ?? -1;
var item = new DispatchChecklistItem
{
DispatchId = model.DispatchId,
WorkOrderId = model.WorkOrderId,
ItemText = model.ItemText,
SortOrder = maxSort + 1,
IsCompleted = false
};
_db.DispatchChecklistItems.Add(item);
await _db.SaveChangesAsync();
return Ok(new { item.Id, item.ItemText, item.SortOrder, item.IsCompleted });
}
private async Task<string> GenerateInternalWONumber()
{
var lastWO = await _db.workOrders

View file

@ -7,5 +7,21 @@ namespace Api.SeaHavenIndustries.DTOs
public decimal? NTEAmount { get; set; }
public DateTime? ScheduledDate { get; set; }
public string? Description { get; set; }
public int? TaskListTemplateId { get; set; }
public List<string>? CustomChecklistItems { get; set; }
}
public class ChecklistItemUpdate_DTO
{
public int Id { get; set; }
public bool IsCompleted { get; set; }
public string? CompletedBy { get; set; }
}
public class AddChecklistItem_DTO
{
public int DispatchId { get; set; }
public int? WorkOrderId { get; set; }
public string? ItemText { get; set; }
}
}

View file

@ -42,6 +42,7 @@ namespace Data.SeaHavenIndustries
public DbSet<Vendor> Vendors { get; set; }
public DbSet<Dispatch> Dispatches { get; set; }
public DbSet<DispatchWorkOrder> DispatchWorkOrders { get; set; }
public DbSet<DispatchChecklistItem> DispatchChecklistItems { 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,70 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <inheritdoc />
public partial class AddDispatchChecklist : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DispatchChecklistItems",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DispatchId = table.Column<int>(type: "int", nullable: false),
WorkOrderId = table.Column<int>(type: "int", nullable: true),
ItemText = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsCompleted = table.Column<bool>(type: "bit", nullable: false),
CompletedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
CompletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
SortOrder = table.Column<int>(type: "int", nullable: false),
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_DispatchChecklistItems", x => x.Id);
table.ForeignKey(
name: "FK_DispatchChecklistItems_Dispatches_DispatchId",
column: x => x.DispatchId,
principalTable: "Dispatches",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_DispatchChecklistItems_workOrders_WorkOrderId",
column: x => x.WorkOrderId,
principalTable: "workOrders",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_DispatchChecklistItems_DispatchId",
table: "DispatchChecklistItems",
column: "DispatchId");
migrationBuilder.CreateIndex(
name: "IX_DispatchChecklistItems_WorkOrderId",
table: "DispatchChecklistItems",
column: "WorkOrderId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DispatchChecklistItems");
}
}
}

View file

@ -712,6 +712,65 @@ namespace Data.SeaHavenIndustries.Migrations
b.ToTable("Dispatches");
});
modelBuilder.Entity("Data.SeaHavenIndustries.DispatchChecklistItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime?>("CompletedAt")
.HasColumnType("datetime2");
b.Property<string>("CompletedBy")
.HasColumnType("nvarchar(max)");
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>("IsCompleted")
.HasColumnType("bit");
b.Property<bool?>("IsDeleted")
.HasColumnType("bit");
b.Property<string>("ItemText")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2");
b.Property<int?>("LastModifierUserId")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<int?>("WorkOrderId")
.HasColumnType("int");
b.Property<string>("createdby")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("DispatchId");
b.HasIndex("WorkOrderId");
b.ToTable("DispatchChecklistItems");
});
modelBuilder.Entity("Data.SeaHavenIndustries.DispatchWorkOrder", b =>
{
b.Property<int>("Id")
@ -2217,6 +2276,24 @@ namespace Data.SeaHavenIndustries.Migrations
b.Navigation("WorkOrder");
});
modelBuilder.Entity("Data.SeaHavenIndustries.DispatchChecklistItem", b =>
{
b.HasOne("Data.SeaHavenIndustries.Dispatch", "Dispatch")
.WithMany("ChecklistItems")
.HasForeignKey("DispatchId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder")
.WithMany()
.HasForeignKey("WorkOrderId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Dispatch");
b.Navigation("WorkOrder");
});
modelBuilder.Entity("Data.SeaHavenIndustries.DispatchWorkOrder", b =>
{
b.HasOne("Data.SeaHavenIndustries.Dispatch", "Dispatch")
@ -2588,6 +2665,8 @@ namespace Data.SeaHavenIndustries.Migrations
modelBuilder.Entity("Data.SeaHavenIndustries.Dispatch", b =>
{
b.Navigation("ChecklistItems");
b.Navigation("Comments");
b.Navigation("DispatchWorkOrders");

View file

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

View file

@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Data.SeaHavenIndustries
{
public class DispatchChecklistItem : FullAuditEntity
{
public int DispatchId { get; set; }
[ForeignKey(nameof(DispatchId))]
public virtual Dispatch? Dispatch { get; set; }
public int? WorkOrderId { get; set; }
[ForeignKey(nameof(WorkOrderId))]
public virtual WorkOrder? WorkOrder { get; set; }
public string? ItemText { get; set; }
public bool IsCompleted { get; set; }
public string? CompletedBy { get; set; }
public DateTime? CompletedAt { get; set; }
public int SortOrder { get; set; }
}
}