mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 04:53:11 +00:00
Add multi-WO dispatch support with junction table
- DispatchWorkOrder junction table for 1:N dispatch-to-WO relationship - Make Dispatch.WorkOrderId nullable (backward compat) - Add AcceptToken and AcknowledgedAt to Dispatch model - Dispatch_DTO accepts WorkOrderIds array - DispatchToVendor creates junction rows, email lists all WOs in table - GetDispatches queries both junction table and direct FK - GetDispatchById includes workOrders list from junction table - Migration with DispatchWorkOrder table
This commit is contained in:
parent
1f4bd12cd7
commit
be190836a4
8 changed files with 2895 additions and 40 deletions
|
|
@ -1031,10 +1031,13 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
{
|
||||
try
|
||||
{
|
||||
var workOrder = await _db.workOrders.Include(w => w.Locations)
|
||||
.FirstOrDefaultAsync(w => w.Id == model.WorkOrderId);
|
||||
if (workOrder == null)
|
||||
return BadRequest(new Response { Status = "Error", Message = "Work order not found" });
|
||||
var workOrders = await _db.workOrders.Include(w => w.Locations)
|
||||
.Where(w => model.WorkOrderIds.Contains(w.Id))
|
||||
.ToListAsync();
|
||||
if (workOrders.Count == 0)
|
||||
return BadRequest(new Response { Status = "Error", Message = "No work orders found" });
|
||||
|
||||
var primaryWO = workOrders.First();
|
||||
|
||||
var lastPO = await _db.Dispatches
|
||||
.Where(d => d.PONumber != null)
|
||||
|
|
@ -1069,11 +1072,11 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
var dispatchNumber = $"DSP-{nextDSP:D5}";
|
||||
nextDSP++;
|
||||
|
||||
var replyTo = $"wo-{workOrder.InternalWONumber}-{dispatchNumber}@int.seahaven.com";
|
||||
var replyTo = $"wo-{primaryWO.InternalWONumber}-{dispatchNumber}@int.seahaven.com";
|
||||
|
||||
var dispatch = new Dispatch
|
||||
{
|
||||
WorkOrderId = model.WorkOrderId,
|
||||
WorkOrderId = primaryWO.Id,
|
||||
VendorId = vendorId,
|
||||
DispatchNumber = dispatchNumber,
|
||||
PONumber = poNumber,
|
||||
|
|
@ -1082,50 +1085,60 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
Status = "Sent",
|
||||
ScheduledDate = model.ScheduledDate,
|
||||
DispatchedAt = DateTime.UtcNow,
|
||||
ReplyToAddress = replyTo
|
||||
ReplyToAddress = replyTo,
|
||||
AcceptToken = Guid.NewGuid().ToString(),
|
||||
DispatchWorkOrders = workOrders.Select(wo => new DispatchWorkOrder { WorkOrderId = wo.Id }).ToList()
|
||||
};
|
||||
|
||||
var subject = $"[WO-{workOrder.InternalWONumber}-{dispatchNumber}] {workOrder.WorkerOrderTitle}";
|
||||
var locationName = workOrder.Locations?.Name ?? "";
|
||||
var locationAddr = workOrder.Locations?.Address ?? "";
|
||||
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>" +
|
||||
$"<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 html = $@"
|
||||
<h2>Work Order Dispatch</h2>
|
||||
<h2>Work Order Dispatch — {dispatchNumber}</h2>
|
||||
<p><strong>Location:</strong> {locationName} {locationAddr}</p>
|
||||
<p><strong>Vendor PO:</strong> {poNumber} <strong>NTE:</strong> ${model.NTEAmount?.ToString("F2") ?? "N/A"}</p>
|
||||
<h3>Work Orders</h3>
|
||||
<table style='border-collapse:collapse;width:100%;font-family:Arial,sans-serif;'>
|
||||
<tr><td style='padding:8px;font-weight:bold;border:1px solid #ddd;'>WO Number</td><td style='padding:8px;border:1px solid #ddd;'>{workOrder.InternalWONumber}</td></tr>
|
||||
<tr><td style='padding:8px;font-weight:bold;border:1px solid #ddd;'>Title</td><td style='padding:8px;border:1px solid #ddd;'>{workOrder.WorkerOrderTitle}</td></tr>
|
||||
<tr><td style='padding:8px;font-weight:bold;border:1px solid #ddd;'>Location</td><td style='padding:8px;border:1px solid #ddd;'>{locationName} {locationAddr}</td></tr>
|
||||
<tr><td style='padding:8px;font-weight:bold;border:1px solid #ddd;'>Priority</td><td style='padding:8px;border:1px solid #ddd;'>{workOrder.Priority}</td></tr>
|
||||
<tr><td style='padding:8px;font-weight:bold;border:1px solid #ddd;'>Due Date</td><td style='padding:8px;border:1px solid #ddd;'>{workOrder.DueDate?.ToString("yyyy-MM-dd") ?? "N/A"}</td></tr>
|
||||
<tr><td style='padding:8px;font-weight:bold;border:1px solid #ddd;'>Trade</td><td style='padding:8px;border:1px solid #ddd;'>{workOrder.Trade}</td></tr>
|
||||
<tr><td style='padding:8px;font-weight:bold;border:1px solid #ddd;'>Sub-Trade</td><td style='padding:8px;border:1px solid #ddd;'>{workOrder.SubTrade}</td></tr>
|
||||
<tr><td style='padding:8px;font-weight:bold;border:1px solid #ddd;'>Problem</td><td style='padding:8px;border:1px solid #ddd;'>{workOrder.Problem}</td></tr>
|
||||
<tr><td style='padding:8px;font-weight:bold;border:1px solid #ddd;'>NTE</td><td style='padding:8px;border:1px solid #ddd;'>${model.NTEAmount?.ToString("F2") ?? "N/A"}</td></tr>
|
||||
<tr><td style='padding:8px;font-weight:bold;border:1px solid #ddd;'>Vendor PO</td><td style='padding:8px;border:1px solid #ddd;'>{poNumber}</td></tr>
|
||||
<tr style='background:#f5f5f5;'>
|
||||
<th style='padding:6px 8px;border:1px solid #ddd;text-align:left;'>WO #</th>
|
||||
<th style='padding:6px 8px;border:1px solid #ddd;text-align:left;'>Title</th>
|
||||
<th style='padding:6px 8px;border:1px solid #ddd;text-align:left;'>Priority</th>
|
||||
<th style='padding:6px 8px;border:1px solid #ddd;text-align:left;'>Due Date</th>
|
||||
</tr>
|
||||
{woListHtml}
|
||||
</table>
|
||||
<h3>Description</h3>
|
||||
<p>{model.Description ?? workOrder.Description}</p>
|
||||
<hr/>
|
||||
<p>{model.Description ?? primaryWO.Description}</p>
|
||||
<br/><p style='color:#666;font-size:13px;'>{senderName}<br/>Sea Haven Industries</p>
|
||||
<hr/><p style='color:#999;font-size:12px;'>Reply to this email to communicate about this work order.</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);
|
||||
dispatch.EmailSent = emailSent;
|
||||
|
||||
_db.Dispatches.Add(dispatch);
|
||||
|
||||
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
|
||||
foreach (var wo in workOrders)
|
||||
{
|
||||
WorkOrderId = model.WorkOrderId,
|
||||
UserId = userId,
|
||||
FieldName = "Dispatch",
|
||||
OldValue = "",
|
||||
NewValue = $"{vendor.CompanyName} ({poNumber})",
|
||||
Action = "dispatch",
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
|
||||
{
|
||||
WorkOrderId = wo.Id,
|
||||
UserId = userId,
|
||||
FieldName = "Dispatch",
|
||||
OldValue = "",
|
||||
NewValue = $"{vendor.CompanyName} ({dispatchNumber})",
|
||||
Action = "dispatch",
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
dispatches.Add(new { dispatch.Id, dispatchNumber, poNumber, vendorName = vendor.CompanyName, dispatch.Status });
|
||||
dispatches.Add(new { dispatch.Id, dispatchNumber, poNumber, vendorName = vendor.CompanyName, dispatch.Status, workOrderCount = workOrders.Count });
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
|
@ -1140,8 +1153,21 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
[HttpGet("GetDispatches/{workOrderId}")]
|
||||
public async Task<IActionResult> GetDispatches(int workOrderId)
|
||||
{
|
||||
var data = await _db.Dispatches
|
||||
var dispatchIds = await _db.DispatchWorkOrders
|
||||
.Where(dwo => dwo.WorkOrderId == workOrderId)
|
||||
.Select(dwo => dwo.DispatchId)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
var directIds = await _db.Dispatches
|
||||
.Where(d => d.WorkOrderId == workOrderId)
|
||||
.Select(d => d.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var allIds = dispatchIds.Union(directIds).Distinct().ToList();
|
||||
|
||||
var data = await _db.Dispatches
|
||||
.Where(d => allIds.Contains(d.Id))
|
||||
.Include(d => d.Vendor)
|
||||
.OrderByDescending(d => d.DispatchedAt)
|
||||
.Select(d => new
|
||||
|
|
@ -1184,11 +1210,19 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
x.DispatchedAt,
|
||||
x.EmailSent,
|
||||
x.WorkOrderId,
|
||||
x.AcknowledgedAt,
|
||||
woNumber = x.WorkOrder != null ? x.WorkOrder.InternalWONumber : "",
|
||||
woTitle = x.WorkOrder != null ? x.WorkOrder.WorkerOrderTitle : "",
|
||||
vendorName = x.Vendor != null ? x.Vendor.CompanyName : "",
|
||||
vendorEmail = x.Vendor != null ? x.Vendor.Email : "",
|
||||
vendorId = x.VendorId,
|
||||
workOrders = _db.DispatchWorkOrders
|
||||
.Where(dwo => dwo.DispatchId == x.Id)
|
||||
.Select(dwo => new {
|
||||
dwo.WorkOrderId,
|
||||
woNumber = dwo.WorkOrder != null ? dwo.WorkOrder.InternalWONumber : "",
|
||||
woTitle = dwo.WorkOrder != null ? dwo.WorkOrder.WorkerOrderTitle : ""
|
||||
}).ToList(),
|
||||
comments = _db.Comments
|
||||
.Where(c => c.DispatchId == x.Id)
|
||||
.OrderBy(c => c.CreatedDate)
|
||||
|
|
@ -1223,7 +1257,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
{
|
||||
_db.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
|
||||
{
|
||||
WorkOrderId = dispatch.WorkOrderId,
|
||||
WorkOrderId = dispatch.WorkOrderId ?? 0,
|
||||
UserId = userId,
|
||||
FieldName = $"Dispatch {dispatch.DispatchNumber} Status",
|
||||
OldValue = dispatch.Status ?? "",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ namespace Api.SeaHavenIndustries.DTOs
|
|||
{
|
||||
public class Dispatch_DTO
|
||||
{
|
||||
public int WorkOrderId { get; set; }
|
||||
public List<int> WorkOrderIds { get; set; } = new();
|
||||
public List<int> VendorIds { get; set; } = new();
|
||||
public decimal? NTEAmount { get; set; }
|
||||
public DateTime? ScheduledDate { get; set; }
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ namespace Data.SeaHavenIndustries
|
|||
public DbSet<DropdownOption> DropdownOptions { get; set; }
|
||||
public DbSet<Vendor> Vendors { get; set; }
|
||||
public DbSet<Dispatch> Dispatches { get; set; }
|
||||
public DbSet<DispatchWorkOrder> DispatchWorkOrders { get; set; }
|
||||
public DbSet<TaskListTemplate> TaskListTemplates { get; set; }
|
||||
public DbSet<TaskListTemplateItem> TaskListTemplateItems { get; set; }
|
||||
|
||||
|
|
|
|||
2657
Data.SeaHavenIndustries/Migrations/20260417190743_MultiWODispatch.Designer.cs
generated
Normal file
2657
Data.SeaHavenIndustries/Migrations/20260417190743_MultiWODispatch.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,96 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.SeaHavenIndustries.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MultiWODispatch : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "WorkOrderId",
|
||||
table: "Dispatches",
|
||||
type: "int",
|
||||
nullable: true,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "int");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AcceptToken",
|
||||
table: "Dispatches",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "AcknowledgedAt",
|
||||
table: "Dispatches",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DispatchWorkOrders",
|
||||
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: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DispatchWorkOrders", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_DispatchWorkOrders_Dispatches_DispatchId",
|
||||
column: x => x.DispatchId,
|
||||
principalTable: "Dispatches",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_DispatchWorkOrders_workOrders_WorkOrderId",
|
||||
column: x => x.WorkOrderId,
|
||||
principalTable: "workOrders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DispatchWorkOrders_DispatchId",
|
||||
table: "DispatchWorkOrders",
|
||||
column: "DispatchId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DispatchWorkOrders_WorkOrderId",
|
||||
table: "DispatchWorkOrders",
|
||||
column: "WorkOrderId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "DispatchWorkOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AcceptToken",
|
||||
table: "Dispatches");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AcknowledgedAt",
|
||||
table: "Dispatches");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "WorkOrderId",
|
||||
table: "Dispatches",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "int",
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -640,6 +640,12 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("AcceptToken")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("AcknowledgedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("CompletedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
|
|
@ -691,7 +697,7 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.Property<int>("VendorId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("WorkOrderId")
|
||||
b.Property<int?>("WorkOrderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("createdby")
|
||||
|
|
@ -706,6 +712,29 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.ToTable("Dispatches");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.DispatchWorkOrder", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("DispatchId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("WorkOrderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DispatchId");
|
||||
|
||||
b.HasIndex("WorkOrderId");
|
||||
|
||||
b.ToTable("DispatchWorkOrders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.DropdownOption", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
@ -2181,10 +2210,28 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder")
|
||||
.WithMany("Dispatches")
|
||||
.HasForeignKey("WorkOrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("Vendor");
|
||||
|
||||
b.Navigation("WorkOrder");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.DispatchWorkOrder", b =>
|
||||
{
|
||||
b.HasOne("Data.SeaHavenIndustries.Dispatch", "Dispatch")
|
||||
.WithMany("DispatchWorkOrders")
|
||||
.HasForeignKey("DispatchId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Vendor");
|
||||
b.HasOne("Data.SeaHavenIndustries.WorkOrder", "WorkOrder")
|
||||
.WithMany()
|
||||
.HasForeignKey("WorkOrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Dispatch");
|
||||
|
||||
b.Navigation("WorkOrder");
|
||||
});
|
||||
|
|
@ -2542,6 +2589,8 @@ namespace Data.SeaHavenIndustries.Migrations
|
|||
modelBuilder.Entity("Data.SeaHavenIndustries.Dispatch", b =>
|
||||
{
|
||||
b.Navigation("Comments");
|
||||
|
||||
b.Navigation("DispatchWorkOrders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Data.SeaHavenIndustries.Employee", b =>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ namespace Data.SeaHavenIndustries
|
|||
{
|
||||
public class Dispatch : FullAuditEntity
|
||||
{
|
||||
public int WorkOrderId { get; set; }
|
||||
public int? WorkOrderId { get; set; }
|
||||
[ForeignKey(nameof(WorkOrderId))]
|
||||
public virtual WorkOrder? WorkOrder { get; set; }
|
||||
|
||||
|
|
@ -20,8 +20,11 @@ namespace Data.SeaHavenIndustries
|
|||
public DateTime? ScheduledDate { get; set; }
|
||||
public DateTime? CompletedDate { get; set; }
|
||||
public DateTime? DispatchedAt { get; set; }
|
||||
public DateTime? AcknowledgedAt { get; set; }
|
||||
public bool EmailSent { get; set; }
|
||||
public string? ReplyToAddress { get; set; }
|
||||
public string? AcceptToken { get; set; }
|
||||
public ICollection<Comments>? Comments { get; set; }
|
||||
public ICollection<DispatchWorkOrder>? DispatchWorkOrders { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
15
Data.SeaHavenIndustries/Models/DispatchWorkOrder.cs
Normal file
15
Data.SeaHavenIndustries/Models/DispatchWorkOrder.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Data.SeaHavenIndustries
|
||||
{
|
||||
public class DispatchWorkOrder
|
||||
{
|
||||
public int Id { get; set; }
|
||||
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; }
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue