mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-06 17:02:12 +00:00
Merge pull request #63 from Sea-Haven-Industries/feature/sh-188-board-pending-uplift
feat(work-orders): expose pendingUpliftCount on board rows (SH-188)
This commit is contained in:
commit
5f614fe893
9 changed files with 279 additions and 4 deletions
|
|
@ -7,6 +7,7 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
public static class WorkOrderBoardProjection
|
public static class WorkOrderBoardProjection
|
||||||
{
|
{
|
||||||
public static async Task<IReadOnlyList<WorkOrderBoardRawRow>> ProjectRowsAsync(
|
public static async Task<IReadOnlyList<WorkOrderBoardRawRow>> ProjectRowsAsync(
|
||||||
|
ApplicationDbContext context,
|
||||||
IQueryable<WorkOrder> query,
|
IQueryable<WorkOrder> query,
|
||||||
bool isUnscheduled)
|
bool isUnscheduled)
|
||||||
{
|
{
|
||||||
|
|
@ -70,7 +71,17 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
w.FlagColor,
|
w.FlagColor,
|
||||||
w.PrimaryDispatchId,
|
w.PrimaryDispatchId,
|
||||||
w.RowVersion,
|
w.RowVersion,
|
||||||
DispatchRowVersion = w.PrimaryDispatch != null ? w.PrimaryDispatch.RowVersion : null
|
DispatchRowVersion = w.PrimaryDispatch != null ? w.PrimaryDispatch.RowVersion : null,
|
||||||
|
// SH-188: aggregate Pending uplifts on every live dispatch tied to this WO
|
||||||
|
// (primary WorkOrderId and multi-WO DispatchWorkOrders links).
|
||||||
|
PendingUpliftCount = context.DispatchUpliftRequests.Count(u =>
|
||||||
|
u.Status == "Pending"
|
||||||
|
&& (u.IsDeleted == null || u.IsDeleted == false)
|
||||||
|
&& u.Dispatch != null
|
||||||
|
&& (u.Dispatch.IsDeleted == null || u.Dispatch.IsDeleted == false)
|
||||||
|
&& (
|
||||||
|
u.Dispatch.WorkOrderId == w.Id
|
||||||
|
|| u.Dispatch.DispatchWorkOrders!.Any(link => link.WorkOrderId == w.Id)))
|
||||||
})
|
})
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
|
@ -122,6 +133,7 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
w.PrimaryDispatchId,
|
w.PrimaryDispatchId,
|
||||||
w.RowVersion,
|
w.RowVersion,
|
||||||
w.DispatchRowVersion,
|
w.DispatchRowVersion,
|
||||||
|
w.PendingUpliftCount,
|
||||||
isUnscheduled);
|
isUnscheduled);
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
.Skip(query.Page * query.PageSize)
|
.Skip(query.Page * query.PageSize)
|
||||||
.Take(query.PageSize);
|
.Take(query.PageSize);
|
||||||
|
|
||||||
var rows = await WorkOrderBoardProjection.ProjectRowsAsync(paged, isUnscheduled: false);
|
var rows = await WorkOrderBoardProjection.ProjectRowsAsync(_context, paged, isUnscheduled: false);
|
||||||
return new WorkOrderAdvancedSearchResult(rows, totalCount);
|
return new WorkOrderAdvancedSearchResult(rows, totalCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,9 +47,11 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
var scheduledRows = await WorkOrderBoardProjection.ProjectRowsAsync(
|
var scheduledRows = await WorkOrderBoardProjection.ProjectRowsAsync(
|
||||||
|
_context,
|
||||||
scheduledBase.OrderBy(w => w.ScheduledDate).ThenBy(w => w.InternalWONumber),
|
scheduledBase.OrderBy(w => w.ScheduledDate).ThenBy(w => w.InternalWONumber),
|
||||||
isUnscheduled: false);
|
isUnscheduled: false);
|
||||||
var unscheduledRows = await WorkOrderBoardProjection.ProjectRowsAsync(
|
var unscheduledRows = await WorkOrderBoardProjection.ProjectRowsAsync(
|
||||||
|
_context,
|
||||||
unscheduledBase.OrderBy(w => w.InternalWONumber),
|
unscheduledBase.OrderBy(w => w.InternalWONumber),
|
||||||
isUnscheduled: true);
|
isUnscheduled: true);
|
||||||
|
|
||||||
|
|
@ -79,7 +81,7 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
if (accountId.HasValue)
|
if (accountId.HasValue)
|
||||||
query = WorkOrderBoardQueryFilters.ApplyAccountScope(query, accountId.Value);
|
query = WorkOrderBoardQueryFilters.ApplyAccountScope(query, accountId.Value);
|
||||||
|
|
||||||
var rows = await WorkOrderBoardProjection.ProjectRowsAsync(query, isUnscheduled: false);
|
var rows = await WorkOrderBoardProjection.ProjectRowsAsync(_context, query, isUnscheduled: false);
|
||||||
return rows.FirstOrDefault();
|
return rows.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
int? PrimaryDispatchId,
|
int? PrimaryDispatchId,
|
||||||
byte[]? RowVersion,
|
byte[]? RowVersion,
|
||||||
byte[]? DispatchRowVersion,
|
byte[]? DispatchRowVersion,
|
||||||
|
int PendingUpliftCount,
|
||||||
bool IsUnscheduled);
|
bool IsUnscheduled);
|
||||||
|
|
||||||
public record WorkOrderBoardQueryResult(
|
public record WorkOrderBoardQueryResult(
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,11 @@ namespace SeaHaven.Services.DTOs
|
||||||
public int? PrimaryDispatchId { get; set; }
|
public int? PrimaryDispatchId { get; set; }
|
||||||
public byte[]? RowVersion { get; set; }
|
public byte[]? RowVersion { get; set; }
|
||||||
public byte[]? DispatchRowVersion { get; set; }
|
public byte[]? DispatchRowVersion { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Count of uplift requests with status Pending across all dispatches tied to this work order (SH-188).
|
||||||
|
/// Authoritative completion-gate signal for the board/search/detail row contract.
|
||||||
|
/// </summary>
|
||||||
|
public int PendingUpliftCount { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class WorkOrderBoardPatchRequestDto
|
public class WorkOrderBoardPatchRequestDto
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,8 @@ namespace SeaHaven.Services.Implementation
|
||||||
FlagColor = row.FlagColor,
|
FlagColor = row.FlagColor,
|
||||||
PrimaryDispatchId = row.PrimaryDispatchId,
|
PrimaryDispatchId = row.PrimaryDispatchId,
|
||||||
RowVersion = row.RowVersion,
|
RowVersion = row.RowVersion,
|
||||||
DispatchRowVersion = row.DispatchRowVersion
|
DispatchRowVersion = row.DispatchRowVersion,
|
||||||
|
PendingUpliftCount = row.PendingUpliftCount
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
PrimaryDispatchId = row.PrimaryDispatchId,
|
PrimaryDispatchId = row.PrimaryDispatchId,
|
||||||
RowVersion = row.RowVersion,
|
RowVersion = row.RowVersion,
|
||||||
DispatchRowVersion = row.DispatchRowVersion,
|
DispatchRowVersion = row.DispatchRowVersion,
|
||||||
|
PendingUpliftCount = row.PendingUpliftCount,
|
||||||
Description = extended?.Description,
|
Description = extended?.Description,
|
||||||
Trade = extended?.Trade,
|
Trade = extended?.Trade,
|
||||||
Problem = extended?.Problem,
|
Problem = extended?.Problem,
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,219 @@ public class WorkOrderBoardDataServiceTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class WorkOrderBoardPendingUpliftTests
|
||||||
|
{
|
||||||
|
private static ApplicationDbContext CreateContext()
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||||
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||||
|
.Options;
|
||||||
|
return new ApplicationDbContext(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBoardRows_PendingUpliftOnPrimaryDispatch_SurfacesCount()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
var weekStart = new DateOnly(2026, 6, 22);
|
||||||
|
|
||||||
|
context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Acme HVAC" });
|
||||||
|
context.Dispatches.Add(new Dispatch
|
||||||
|
{
|
||||||
|
Id = 10,
|
||||||
|
VendorId = 1,
|
||||||
|
WorkOrderId = 1,
|
||||||
|
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"
|
||||||
|
});
|
||||||
|
context.DispatchUpliftRequests.Add(new DispatchUpliftRequest
|
||||||
|
{
|
||||||
|
Id = 100,
|
||||||
|
DispatchId = 10,
|
||||||
|
RequestedNTE = 1500m,
|
||||||
|
Status = "Pending",
|
||||||
|
RequiredTier = 1,
|
||||||
|
NotificationStatus = "Pending"
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var service = new WorkOrderBoardDataService(context);
|
||||||
|
var result = await service.GetBoardRowsAsync(new WorkOrderBoardQuery(
|
||||||
|
weekStart,
|
||||||
|
weekStart.AddDays(4),
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null));
|
||||||
|
|
||||||
|
Assert.Single(result.ScheduledRows);
|
||||||
|
Assert.Equal(1, result.ScheduledRows[0].PendingUpliftCount);
|
||||||
|
|
||||||
|
var dto = WorkOrderBoardService.MapRawRow(result.ScheduledRows[0], DateTime.UtcNow);
|
||||||
|
Assert.Equal(1, dto.PendingUpliftCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBoardRows_ApprovedUplift_DoesNotBlock()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
var weekStart = new DateOnly(2026, 6, 22);
|
||||||
|
|
||||||
|
context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Acme HVAC" });
|
||||||
|
context.Dispatches.Add(new Dispatch
|
||||||
|
{
|
||||||
|
Id = 10,
|
||||||
|
VendorId = 1,
|
||||||
|
WorkOrderId = 1
|
||||||
|
});
|
||||||
|
context.workOrders.Add(new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
InternalWONumber = "10000000001",
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 23),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||||
|
PrimaryDispatchId = 10
|
||||||
|
});
|
||||||
|
context.DispatchUpliftRequests.Add(new DispatchUpliftRequest
|
||||||
|
{
|
||||||
|
Id = 100,
|
||||||
|
DispatchId = 10,
|
||||||
|
RequestedNTE = 1500m,
|
||||||
|
Status = "Approved",
|
||||||
|
RequiredTier = 1,
|
||||||
|
NotificationStatus = "Sent"
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var service = new WorkOrderBoardDataService(context);
|
||||||
|
var result = await service.GetBoardRowsAsync(new WorkOrderBoardQuery(
|
||||||
|
weekStart,
|
||||||
|
weekStart.AddDays(4),
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null));
|
||||||
|
|
||||||
|
Assert.Single(result.ScheduledRows);
|
||||||
|
Assert.Equal(0, result.ScheduledRows[0].PendingUpliftCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBoardRows_PendingUpliftViaDispatchWorkOrderLink_SurfacesCount()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
var weekStart = new DateOnly(2026, 6, 22);
|
||||||
|
|
||||||
|
context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Acme HVAC" });
|
||||||
|
// Secondary link only via DispatchWorkOrders (no WorkOrderId / PrimaryDispatch).
|
||||||
|
context.Dispatches.Add(new Dispatch
|
||||||
|
{
|
||||||
|
Id = 20,
|
||||||
|
VendorId = 1,
|
||||||
|
WorkOrderId = null
|
||||||
|
});
|
||||||
|
context.DispatchWorkOrders.Add(new DispatchWorkOrder
|
||||||
|
{
|
||||||
|
DispatchId = 20,
|
||||||
|
WorkOrderId = 2
|
||||||
|
});
|
||||||
|
context.workOrders.Add(new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
InternalWONumber = "10000000002",
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 24),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled
|
||||||
|
});
|
||||||
|
context.DispatchUpliftRequests.Add(new DispatchUpliftRequest
|
||||||
|
{
|
||||||
|
Id = 200,
|
||||||
|
DispatchId = 20,
|
||||||
|
RequestedNTE = 900m,
|
||||||
|
Status = "Pending",
|
||||||
|
RequiredTier = 1,
|
||||||
|
NotificationStatus = "Pending"
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var service = new WorkOrderBoardDataService(context);
|
||||||
|
var result = await service.GetBoardRowsAsync(new WorkOrderBoardQuery(
|
||||||
|
weekStart,
|
||||||
|
weekStart.AddDays(4),
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null));
|
||||||
|
|
||||||
|
Assert.Single(result.ScheduledRows);
|
||||||
|
Assert.Equal(2, result.ScheduledRows[0].Id);
|
||||||
|
Assert.Equal(1, result.ScheduledRows[0].PendingUpliftCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBoardRows_PendingUpliftOnSoftDeletedDispatch_DoesNotCount()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
var weekStart = new DateOnly(2026, 6, 22);
|
||||||
|
|
||||||
|
context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Acme HVAC" });
|
||||||
|
context.Dispatches.Add(new Dispatch
|
||||||
|
{
|
||||||
|
Id = 10,
|
||||||
|
VendorId = 1,
|
||||||
|
WorkOrderId = 1,
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 24, 8, 0, 0),
|
||||||
|
IsDeleted = true
|
||||||
|
});
|
||||||
|
context.workOrders.Add(new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
InternalWONumber = "10000000001",
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 23),
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||||
|
PrimaryDispatchId = 10,
|
||||||
|
SiteCode = "BK5"
|
||||||
|
});
|
||||||
|
context.DispatchUpliftRequests.Add(new DispatchUpliftRequest
|
||||||
|
{
|
||||||
|
Id = 100,
|
||||||
|
DispatchId = 10,
|
||||||
|
RequestedNTE = 1500m,
|
||||||
|
Status = "Pending",
|
||||||
|
RequiredTier = 1,
|
||||||
|
NotificationStatus = "Pending"
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var service = new WorkOrderBoardDataService(context);
|
||||||
|
var result = await service.GetBoardRowsAsync(new WorkOrderBoardQuery(
|
||||||
|
weekStart,
|
||||||
|
weekStart.AddDays(4),
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null));
|
||||||
|
|
||||||
|
Assert.Single(result.ScheduledRows);
|
||||||
|
Assert.Equal(0, result.ScheduledRows[0].PendingUpliftCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class WorkOrderBoardServiceTests
|
public class WorkOrderBoardServiceTests
|
||||||
{
|
{
|
||||||
private static ApplicationDbContext CreateContext()
|
private static ApplicationDbContext CreateContext()
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,46 @@ public class WorkOrderDetailServiceTests
|
||||||
Assert.Equal("WeekRolled", detail.Audit[0].Action);
|
Assert.Equal("WeekRolled", detail.Audit[0].Action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDetail_SurfacesPendingUpliftCountFromBoardRow()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
|
||||||
|
context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Acme HVAC" });
|
||||||
|
context.Dispatches.Add(new Dispatch
|
||||||
|
{
|
||||||
|
Id = 10,
|
||||||
|
VendorId = 1,
|
||||||
|
WorkOrderId = 1,
|
||||||
|
ScheduledDate = new DateTime(2026, 6, 24, 8, 0, 0)
|
||||||
|
});
|
||||||
|
context.workOrders.Add(new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
InternalWONumber = "10000000001",
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||||
|
ScheduledDate = DateTime.UtcNow.AddDays(1),
|
||||||
|
PrimaryDispatchId = 10,
|
||||||
|
SiteCode = "BK5",
|
||||||
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||||
|
});
|
||||||
|
context.DispatchUpliftRequests.Add(new DispatchUpliftRequest
|
||||||
|
{
|
||||||
|
Id = 100,
|
||||||
|
DispatchId = 10,
|
||||||
|
RequestedNTE = 1500m,
|
||||||
|
Status = "Pending",
|
||||||
|
RequiredTier = 1,
|
||||||
|
NotificationStatus = "Pending"
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var detail = await service.GetDetailAsync(1, WorkOrderAccountTestHelpers.OrgWideAdmin());
|
||||||
|
|
||||||
|
Assert.NotNull(detail);
|
||||||
|
Assert.Equal(1, detail!.Info.PendingUpliftCount);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetAudit_ResolvesAssignToUserIdsToDisplayNames()
|
public async Task GetAudit_ResolvesAssignToUserIdsToDisplayNames()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue