diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardDispatchAssignment.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardDispatchAssignment.cs
new file mode 100644
index 0000000..b257b66
--- /dev/null
+++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardDispatchAssignment.cs
@@ -0,0 +1,28 @@
+namespace SeaHaven.DataServices.Helpers
+{
+ ///
+ /// SH-183: a cancelled or refused primary dispatch is not a live vendor assignment.
+ /// Verified/Completed remain live — the company that did the work.
+ ///
+ public static class WorkOrderBoardDispatchAssignment
+ {
+ public static bool IsInactivePrimaryDispatch(string? status)
+ {
+ var normalized = (status ?? string.Empty).Trim().ToLowerInvariant();
+ return normalized is "cancelled" or "canceled" or "refused";
+ }
+
+ public static (int? VendorId, string? VendorName) LiveVendor(
+ int? vendorId,
+ string? vendorName,
+ string? primaryDispatchStatus)
+ {
+ if (IsInactivePrimaryDispatch(primaryDispatchStatus))
+ {
+ return (null, null);
+ }
+
+ return (vendorId, vendorName);
+ }
+ }
+}
diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs
index 04feeca..480d534 100644
--- a/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs
+++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardProjection.cs
@@ -50,6 +50,7 @@ namespace SeaHaven.DataServices.Helpers
w.ScheduledEnd,
w.TargetWeek,
w.ScheduleWeekOnly,
+ PrimaryDispatchStatus = w.PrimaryDispatch != null ? w.PrimaryDispatch.Status : null,
VendorId = w.PrimaryDispatch != null ? (int?)w.PrimaryDispatch.VendorId : null,
VendorName = w.PrimaryDispatch != null && w.PrimaryDispatch.Vendor != null
? w.PrimaryDispatch.Vendor.CompanyName
@@ -124,6 +125,10 @@ namespace SeaHaven.DataServices.Helpers
var techPhone = !string.IsNullOrWhiteSpace(w.DispatchTechPhone)
? w.DispatchTechPhone
: (!string.IsNullOrWhiteSpace(w.WoTechPhone) ? w.WoTechPhone : w.VendorPhone);
+ var (vendorId, vendorName) = WorkOrderBoardDispatchAssignment.LiveVendor(
+ w.VendorId,
+ w.VendorName,
+ w.PrimaryDispatchStatus);
return new WorkOrderBoardRawRow(
w.Id,
@@ -150,8 +155,8 @@ namespace SeaHaven.DataServices.Helpers
w.ScheduledEnd,
w.TargetWeek,
w.ScheduleWeekOnly,
- w.VendorId,
- w.VendorName,
+ vendorId,
+ vendorName,
w.TechName,
techPhone,
w.DispatchApptDate,
@@ -170,6 +175,7 @@ namespace SeaHaven.DataServices.Helpers
w.HasUplift,
w.PrimaryUpliftStatus,
w.PrimaryUpliftAmount,
+ w.PrimaryDispatchStatus,
isUnscheduled);
}).ToList();
}
diff --git a/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs b/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs
index f122889..6ae2928 100644
--- a/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs
+++ b/SeaHaven.DataServices/Interfaces/WorkOrderBoardModels.cs
@@ -58,6 +58,7 @@ namespace SeaHaven.DataServices.Interfaces
bool HasUplift,
string? PrimaryUpliftStatus,
decimal? PrimaryUpliftAmount,
+ string? PrimaryDispatchStatus,
bool IsUnscheduled);
public record WorkOrderBoardQueryResult(
diff --git a/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs b/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs
index e59464c..865ef7a 100644
--- a/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs
+++ b/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs
@@ -58,6 +58,10 @@ namespace SeaHaven.Services.DTOs
public string? DayGroup { get; set; }
public int? VendorId { get; set; }
public string? VendorName { get; set; }
+ ///
+ /// Primary dispatch status (SH-183). Inactive statuses are not a live company assignment.
+ ///
+ public string? PrimaryDispatchStatus { get; set; }
public string? TechName { get; set; }
public string? TechPhone { get; set; }
public DateTime? ApptDate { get; set; }
diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs
index 4aa70ad..c1b2f42 100644
--- a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs
+++ b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs
@@ -1,5 +1,6 @@
using System.Security.Claims;
using Data.SeaHavenIndustries.Enums;
+using SeaHaven.DataServices.Helpers;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Helpers;
@@ -86,6 +87,10 @@ namespace SeaHaven.Services.Implementation
// Time prefers WO ScheduledStart/End (dispatcher window), then dispatch datetime.
var apptStart = row.ScheduledStart ?? row.DispatchApptDate;
var apptEnd = row.ScheduledEnd;
+ var (vendorId, vendorName) = WorkOrderBoardDispatchAssignment.LiveVendor(
+ row.VendorId,
+ row.VendorName,
+ row.PrimaryDispatchStatus);
return new WorkOrderBoardRowDto
{
@@ -113,8 +118,9 @@ namespace SeaHaven.Services.Implementation
TargetWeek = row.TargetWeek,
ScheduleWeekOnly = row.ScheduleWeekOnly,
DayGroup = WorkOrderDerivedFields.GetDayGroup(row.ScheduledDate),
- VendorId = row.VendorId,
- VendorName = row.VendorName,
+ VendorId = vendorId,
+ VendorName = vendorName,
+ PrimaryDispatchStatus = row.PrimaryDispatchStatus,
TechName = row.TechName,
TechPhone = row.TechPhone,
ApptDate = row.DispatchApptDate ?? row.ScheduledDate,
diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardDispatchAssignmentTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardDispatchAssignmentTests.cs
new file mode 100644
index 0000000..a49e275
--- /dev/null
+++ b/SeaHavenIndustries.Tests/WorkOrderBoardDispatchAssignmentTests.cs
@@ -0,0 +1,35 @@
+using SeaHaven.DataServices.Helpers;
+
+namespace SeaHavenIndustries.Tests;
+
+public class WorkOrderBoardDispatchAssignmentTests
+{
+ [Theory]
+ [InlineData("Cancelled")]
+ [InlineData("Canceled")]
+ [InlineData("Refused")]
+ [InlineData(" cancelled ")]
+ [InlineData("CANCELED")]
+ [InlineData("refused")]
+ public void LiveVendor_InactiveStatus_ClearsAssignment(string status)
+ {
+ var (vendorId, vendorName) = WorkOrderBoardDispatchAssignment.LiveVendor(45, "Acme HVAC", status);
+ Assert.Null(vendorId);
+ Assert.Null(vendorName);
+ Assert.True(WorkOrderBoardDispatchAssignment.IsInactivePrimaryDispatch(status));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("Sent")]
+ [InlineData("Verified")]
+ [InlineData("Completed")]
+ public void LiveVendor_LiveOrMissingStatus_KeepsVendorIdWhenNameEmpty(string? status)
+ {
+ var (vendorId, vendorName) = WorkOrderBoardDispatchAssignment.LiveVendor(45, null, status);
+ Assert.Equal(45, vendorId);
+ Assert.Null(vendorName);
+ Assert.False(WorkOrderBoardDispatchAssignment.IsInactivePrimaryDispatch(status));
+ }
+}
diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardInactiveVendorTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardInactiveVendorTests.cs
new file mode 100644
index 0000000..d2adc8e
--- /dev/null
+++ b/SeaHavenIndustries.Tests/WorkOrderBoardInactiveVendorTests.cs
@@ -0,0 +1,102 @@
+using Data.SeaHavenIndustries;
+using Data.SeaHavenIndustries.Enums;
+using Microsoft.EntityFrameworkCore;
+using SeaHaven.DataServices.Implementation;
+using SeaHaven.DataServices.Interfaces;
+using SeaHaven.Services.Implementation;
+
+namespace SeaHavenIndustries.Tests;
+
+public class WorkOrderBoardInactiveVendorTests
+{
+ private static ApplicationDbContext CreateContext()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(Guid.NewGuid().ToString())
+ .Options;
+ return new ApplicationDbContext(options);
+ }
+
+ private static async Task SeedPrimaryDispatchAsync(
+ ApplicationDbContext context,
+ string? dispatchStatus,
+ string? companyName)
+ {
+ context.Vendors.Add(new Vendor { Id = 1, CompanyName = companyName });
+ context.Dispatches.Add(new Dispatch
+ {
+ Id = 10,
+ VendorId = 1,
+ WorkOrderId = 1,
+ Status = dispatchStatus,
+ ScheduledDate = new DateTime(2026, 6, 24, 8, 0, 0)
+ });
+ context.workOrders.Add(new WorkOrder
+ {
+ Id = 1,
+ InternalWONumber = "10000000001",
+ ScheduledDate = new DateTime(2026, 6, 23),
+ LifecycleStatus = LifecycleStatus.Scheduled,
+ PrimaryDispatchId = 10,
+ SiteCode = "BK5"
+ });
+ await context.SaveChangesAsync();
+ }
+
+ [Theory]
+ [InlineData("Cancelled")]
+ [InlineData("Canceled")]
+ [InlineData("Refused")]
+ public async Task GetBoardRows_InactivePrimaryDispatch_OmitsVendorAssignment(string status)
+ {
+ await using var context = CreateContext();
+ await SeedPrimaryDispatchAsync(context, status, "Acme HVAC");
+ var weekStart = new DateOnly(2026, 6, 22);
+
+ var result = await new WorkOrderBoardDataService(context).GetBoardRowsAsync(new WorkOrderBoardQuery(
+ weekStart,
+ weekStart.AddDays(4),
+ null,
+ false,
+ null,
+ false,
+ null,
+ null));
+
+ var raw = Assert.Single(result.ScheduledRows);
+ Assert.Null(raw.VendorId);
+ Assert.Null(raw.VendorName);
+ Assert.Equal(status, raw.PrimaryDispatchStatus);
+
+ var dto = WorkOrderBoardService.MapRawRow(raw, DateTime.UtcNow);
+ Assert.Null(dto.VendorId);
+ Assert.Null(dto.VendorName);
+ Assert.Equal(status, dto.PrimaryDispatchStatus);
+ }
+
+ [Fact]
+ public async Task GetBoardRows_LiveDispatchWithEmptyCompanyName_KeepsVendorId()
+ {
+ await using var context = CreateContext();
+ await SeedPrimaryDispatchAsync(context, "Sent", "");
+ var weekStart = new DateOnly(2026, 6, 22);
+
+ var result = await new WorkOrderBoardDataService(context).GetBoardRowsAsync(new WorkOrderBoardQuery(
+ weekStart,
+ weekStart.AddDays(4),
+ null,
+ false,
+ null,
+ false,
+ null,
+ null));
+
+ var raw = Assert.Single(result.ScheduledRows);
+ Assert.Equal(1, raw.VendorId);
+ Assert.Equal("Sent", raw.PrimaryDispatchStatus);
+
+ var dto = WorkOrderBoardService.MapRawRow(raw, DateTime.UtcNow);
+ Assert.Equal(1, dto.VendorId);
+ Assert.Equal("Sent", dto.PrimaryDispatchStatus);
+ }
+}