mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 06:03:12 +00:00
fix(work-orders): keep GET /board to scheduled-in-week rows only (SH-165)
This commit is contained in:
parent
424d0b88bc
commit
04958e6121
10 changed files with 389 additions and 81 deletions
|
|
@ -54,7 +54,8 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
[FromQuery] bool myWorkOrders = false,
|
||||
[FromQuery] List<WorkOrderType>? types = null,
|
||||
[FromQuery] bool overdue = false,
|
||||
[FromQuery] string? search = null)
|
||||
[FromQuery] string? search = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var resolvedWeekEnd = weekEnd ?? weekStart.AddDays(4);
|
||||
var weekValidationError = WorkOrderOperationalWeek.ValidateWeekWindow(weekStart, resolvedWeekEnd);
|
||||
|
|
@ -75,7 +76,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
Search = search
|
||||
};
|
||||
|
||||
var result = await _workOrderBoardService.GetBoardAsync(query, User, userId);
|
||||
var result = await _workOrderBoardService.GetBoardAsync(query, User, userId, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "Forbidden")
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ namespace SeaHaven.DataServices.Helpers
|
|||
public static async Task<IReadOnlyList<WorkOrderBoardRawRow>> ProjectRowsAsync(
|
||||
ApplicationDbContext context,
|
||||
IQueryable<WorkOrder> query,
|
||||
bool isUnscheduled)
|
||||
bool isUnscheduled,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var rows = await query
|
||||
.Select(w => new
|
||||
|
|
@ -74,48 +75,14 @@ namespace SeaHaven.DataServices.Helpers
|
|||
w.FlagColor,
|
||||
w.PrimaryDispatchId,
|
||||
w.RowVersion,
|
||||
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))),
|
||||
HasUplift = context.DispatchUpliftRequests.Any(u =>
|
||||
(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))),
|
||||
PrimaryUpliftStatus = context.DispatchUpliftRequests
|
||||
.Where(u =>
|
||||
(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)))
|
||||
.OrderByDescending(u => u.CreatedDate)
|
||||
.Select(u => u.Status)
|
||||
.FirstOrDefault(),
|
||||
PrimaryUpliftAmount = context.DispatchUpliftRequests
|
||||
.Where(u =>
|
||||
(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)))
|
||||
.OrderByDescending(u => u.CreatedDate)
|
||||
.Select(u => (decimal?)u.RequestedNTE)
|
||||
.FirstOrDefault()
|
||||
DispatchRowVersion = w.PrimaryDispatch != null ? w.PrimaryDispatch.RowVersion : null
|
||||
})
|
||||
.ToListAsync();
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (rows.Count == 0)
|
||||
return Array.Empty<WorkOrderBoardRawRow>();
|
||||
|
||||
var upliftByWorkOrder = await LoadUpliftAggregatesAsync(context, query, cancellationToken);
|
||||
|
||||
return rows.Select(w =>
|
||||
{
|
||||
|
|
@ -129,6 +96,7 @@ namespace SeaHaven.DataServices.Helpers
|
|||
w.VendorId,
|
||||
w.VendorName,
|
||||
w.PrimaryDispatchStatus);
|
||||
upliftByWorkOrder.TryGetValue(w.Id, out var uplift);
|
||||
|
||||
return new WorkOrderBoardRawRow(
|
||||
w.Id,
|
||||
|
|
@ -171,13 +139,133 @@ namespace SeaHaven.DataServices.Helpers
|
|||
w.PrimaryDispatchId,
|
||||
w.RowVersion,
|
||||
w.DispatchRowVersion,
|
||||
w.PendingUpliftCount,
|
||||
w.HasUplift,
|
||||
w.PrimaryUpliftStatus,
|
||||
w.PrimaryUpliftAmount,
|
||||
uplift?.PendingCount ?? 0,
|
||||
uplift?.HasUplift ?? false,
|
||||
uplift?.PrimaryStatus,
|
||||
uplift?.PrimaryAmount,
|
||||
w.PrimaryDispatchStatus,
|
||||
isUnscheduled);
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SH-188: one round-trip for live uplifts on WOs in <paramref name="workOrders"/>
|
||||
/// (primary WorkOrderId and multi-WO DispatchWorkOrders links). Uses the same
|
||||
/// IQueryable as a SQL subquery so large boards do not hit the 2100-parameter cap.
|
||||
/// </summary>
|
||||
private static async Task<Dictionary<int, BoardUpliftAgg>> LoadUpliftAggregatesAsync(
|
||||
ApplicationDbContext context,
|
||||
IQueryable<WorkOrder> workOrders,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var workOrderIds = workOrders.Select(w => w.Id);
|
||||
|
||||
var live = context.DispatchUpliftRequests
|
||||
.AsNoTracking()
|
||||
.Where(u =>
|
||||
(u.IsDeleted == null || u.IsDeleted == false)
|
||||
&& u.Dispatch != null
|
||||
&& (u.Dispatch.IsDeleted == null || u.Dispatch.IsDeleted == false));
|
||||
|
||||
var fromPrimary = live
|
||||
.Where(u =>
|
||||
u.Dispatch!.WorkOrderId != null
|
||||
&& workOrderIds.Contains(u.Dispatch.WorkOrderId.Value))
|
||||
.Select(u => new
|
||||
{
|
||||
UpliftId = u.Id,
|
||||
WorkOrderId = u.Dispatch!.WorkOrderId!.Value,
|
||||
u.Status,
|
||||
u.RequestedNTE,
|
||||
u.CreatedDate
|
||||
});
|
||||
|
||||
var fromLinks = live
|
||||
.SelectMany(
|
||||
u => u.Dispatch!.DispatchWorkOrders!
|
||||
.Where(link => workOrderIds.Contains(link.WorkOrderId)),
|
||||
(u, link) => new
|
||||
{
|
||||
UpliftId = u.Id,
|
||||
WorkOrderId = link.WorkOrderId,
|
||||
u.Status,
|
||||
u.RequestedNTE,
|
||||
u.CreatedDate
|
||||
});
|
||||
|
||||
var links = await fromPrimary.Concat(fromLinks).ToListAsync(cancellationToken);
|
||||
return AggregateUpliftLinks(links.Select(l => new UpliftLinkRow(
|
||||
l.UpliftId,
|
||||
l.WorkOrderId,
|
||||
l.Status,
|
||||
l.RequestedNTE,
|
||||
l.CreatedDate)));
|
||||
}
|
||||
|
||||
private static Dictionary<int, BoardUpliftAgg> AggregateUpliftLinks(IEnumerable<UpliftLinkRow> links)
|
||||
{
|
||||
var seen = new HashSet<(int UpliftId, int WorkOrderId)>();
|
||||
var byWorkOrder = new Dictionary<int, List<UpliftLinkRow>>();
|
||||
|
||||
foreach (var link in links)
|
||||
{
|
||||
if (!seen.Add((link.UpliftId, link.WorkOrderId)))
|
||||
continue;
|
||||
|
||||
if (!byWorkOrder.TryGetValue(link.WorkOrderId, out var group))
|
||||
{
|
||||
group = new List<UpliftLinkRow>();
|
||||
byWorkOrder[link.WorkOrderId] = group;
|
||||
}
|
||||
|
||||
group.Add(link);
|
||||
}
|
||||
|
||||
var result = new Dictionary<int, BoardUpliftAgg>(byWorkOrder.Count);
|
||||
foreach (var (workOrderId, group) in byWorkOrder)
|
||||
{
|
||||
var pendingCount = 0;
|
||||
UpliftLinkRow? primary = null;
|
||||
foreach (var item in group)
|
||||
{
|
||||
if (item.Status == "Pending")
|
||||
pendingCount++;
|
||||
if (primary == null || IsNewer(item.CreatedDate, primary.CreatedDate))
|
||||
primary = item;
|
||||
}
|
||||
|
||||
result[workOrderId] = new BoardUpliftAgg(
|
||||
pendingCount,
|
||||
HasUplift: true,
|
||||
primary?.Status,
|
||||
primary?.RequestedNTE);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool IsNewer(DateTime? candidate, DateTime? current)
|
||||
{
|
||||
if (candidate == current)
|
||||
return false;
|
||||
if (candidate is null)
|
||||
return false;
|
||||
if (current is null)
|
||||
return true;
|
||||
return candidate.Value > current.Value;
|
||||
}
|
||||
|
||||
private sealed record UpliftLinkRow(
|
||||
int UpliftId,
|
||||
int WorkOrderId,
|
||||
string Status,
|
||||
decimal RequestedNTE,
|
||||
DateTime? CreatedDate);
|
||||
|
||||
private sealed record BoardUpliftAgg(
|
||||
int PendingCount,
|
||||
bool HasUplift,
|
||||
string? PrimaryStatus,
|
||||
decimal? PrimaryAmount);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Helpers;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
|
|
@ -15,10 +14,12 @@ namespace SeaHaven.DataServices.Implementation
|
|||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<WorkOrderBoardQueryResult> GetBoardRowsAsync(WorkOrderBoardQuery query)
|
||||
public async Task<WorkOrderBoardQueryResult> GetBoardRowsAsync(
|
||||
WorkOrderBoardQuery query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var weekStartDate = query.WeekStart.ToDateTime(TimeOnly.MinValue);
|
||||
var weekEndDate = query.WeekEnd.ToDateTime(TimeOnly.MinValue);
|
||||
var weekEndExclusive = query.WeekEnd.AddDays(1).ToDateTime(TimeOnly.MinValue);
|
||||
|
||||
var baseQuery = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking());
|
||||
if (query.AccountId.HasValue)
|
||||
|
|
@ -29,33 +30,27 @@ namespace SeaHaven.DataServices.Implementation
|
|||
|
||||
var scheduledBase = baseQuery.Where(w =>
|
||||
(w.ScheduledDate != null
|
||||
&& w.ScheduledDate.Value.Date >= weekStartDate.Date
|
||||
&& w.ScheduledDate.Value.Date <= weekEndDate.Date)
|
||||
&& w.ScheduledDate >= weekStartDate
|
||||
&& w.ScheduledDate < weekEndExclusive)
|
||||
|| (w.ScheduleWeekOnly == true && w.TargetWeek == query.WeekStart));
|
||||
|
||||
var unscheduledBase = baseQuery.Where(w =>
|
||||
w.ScheduledDate == null
|
||||
&& !LifecycleStatusSets.Terminal.Contains(w.LifecycleStatus));
|
||||
|
||||
var scheduledTotal = await scheduledBase.CountAsync();
|
||||
var scheduledTotal = await scheduledBase.CountAsync(cancellationToken);
|
||||
|
||||
var normalizedSearch = WorkOrderBoardSearchFilter.NormalizeSearch(query.Search);
|
||||
if (normalizedSearch != null)
|
||||
{
|
||||
scheduledBase = WorkOrderBoardSearchFilter.Apply(scheduledBase, normalizedSearch);
|
||||
unscheduledBase = WorkOrderBoardSearchFilter.Apply(unscheduledBase, normalizedSearch);
|
||||
}
|
||||
|
||||
var scheduledRows = await WorkOrderBoardProjection.ProjectRowsAsync(
|
||||
_context,
|
||||
scheduledBase.OrderBy(w => w.ScheduledDate).ThenBy(w => w.InternalWONumber),
|
||||
isUnscheduled: false);
|
||||
var unscheduledRows = await WorkOrderBoardProjection.ProjectRowsAsync(
|
||||
_context,
|
||||
unscheduledBase.OrderBy(w => w.InternalWONumber),
|
||||
isUnscheduled: true);
|
||||
isUnscheduled: false,
|
||||
cancellationToken);
|
||||
|
||||
return new WorkOrderBoardQueryResult(scheduledRows, unscheduledRows, scheduledTotal);
|
||||
// Weekly board is scheduled-in-week only. Undated rows belong on GET /board/search.
|
||||
return new WorkOrderBoardQueryResult(
|
||||
scheduledRows,
|
||||
Array.Empty<WorkOrderBoardRawRow>(),
|
||||
scheduledTotal);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<DispatcherLookupRow>> GetDispatcherLookupsAsync()
|
||||
|
|
@ -74,14 +69,18 @@ namespace SeaHaven.DataServices.Implementation
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<WorkOrderBoardRawRow?> GetBoardRowByIdAsync(int workOrderId, int? accountId = null)
|
||||
public async Task<WorkOrderBoardRawRow?> GetBoardRowByIdAsync(
|
||||
int workOrderId,
|
||||
int? accountId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = WorkOrderBoardQueryFilters.ApplyBaseScope(
|
||||
_context.workOrders.AsNoTracking().Where(w => w.Id == workOrderId));
|
||||
if (accountId.HasValue)
|
||||
query = WorkOrderBoardQueryFilters.ApplyAccountScope(query, accountId.Value);
|
||||
|
||||
var rows = await WorkOrderBoardProjection.ProjectRowsAsync(_context, query, isUnscheduled: false);
|
||||
var rows = await WorkOrderBoardProjection.ProjectRowsAsync(
|
||||
_context, query, isUnscheduled: false, cancellationToken);
|
||||
return rows.FirstOrDefault();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,14 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
{
|
||||
public interface IWorkOrderBoardDataService
|
||||
{
|
||||
Task<WorkOrderBoardQueryResult> GetBoardRowsAsync(WorkOrderBoardQuery query);
|
||||
Task<WorkOrderBoardQueryResult> GetBoardRowsAsync(
|
||||
WorkOrderBoardQuery query,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<DispatcherLookupRow>> GetDispatcherLookupsAsync();
|
||||
Task<WorkOrderBoardRawRow?> GetBoardRowByIdAsync(int workOrderId, int? accountId = null);
|
||||
Task<WorkOrderBoardRawRow?> GetBoardRowByIdAsync(
|
||||
int workOrderId,
|
||||
int? accountId = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<bool> InternalWoNumberExistsAsync(string normalizedWoNumber, int excludeWorkOrderId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ namespace SeaHaven.Services.DTOs
|
|||
public string? Search { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scheduled-in-week counts for GET /board. <see cref="Total"/> is the week
|
||||
/// match before the bar search filter; <see cref="Returned"/> is after it.
|
||||
/// Undated work orders are not included.
|
||||
/// </summary>
|
||||
public class WorkOrderBoardCountsDto
|
||||
{
|
||||
public int Returned { get; set; }
|
||||
|
|
@ -111,6 +116,10 @@ namespace SeaHaven.Services.DTOs
|
|||
public DateOnly WeekStart { get; set; }
|
||||
public DateOnly WeekEnd { get; set; }
|
||||
public WorkOrderBoardCountsDto Counts { get; set; } = new();
|
||||
/// <summary>
|
||||
/// Always empty on the weekly board. Undated rows are served by GET /board/search.
|
||||
/// Kept so existing clients can keep reading the field.
|
||||
/// </summary>
|
||||
public List<WorkOrderBoardRowDto> Unscheduled { get; set; } = new();
|
||||
public List<WorkOrderBoardRowDto> Scheduled { get; set; } = new();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace SeaHaven.Services.Implementation
|
|||
public class WorkOrderAdvancedSearchService : IWorkOrderAdvancedSearchService
|
||||
{
|
||||
private const int DefaultPageSize = 100;
|
||||
private const int MaxPageSize = 100;
|
||||
private const int MaxPageSize = 200;
|
||||
|
||||
private readonly IWorkOrderAdvancedSearchDataService _searchDataService;
|
||||
private readonly IWorkOrderAccountResolver _accountResolver;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ namespace SeaHaven.Services.Implementation
|
|||
public async Task<WorkOrderBoardResponseDto> GetBoardAsync(
|
||||
WorkOrderBoardQueryDto query,
|
||||
ClaimsPrincipal user,
|
||||
string? currentUserId)
|
||||
string? currentUserId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var accountId = _accountResolver.ResolveAccountFilter(user);
|
||||
var weekStart = query.WeekStart;
|
||||
|
|
@ -45,7 +46,7 @@ namespace SeaHaven.Services.Implementation
|
|||
currentUserId,
|
||||
accountId);
|
||||
|
||||
var result = await _boardDataService.GetBoardRowsAsync(dataQuery);
|
||||
var result = await _boardDataService.GetBoardRowsAsync(dataQuery, cancellationToken);
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
return new WorkOrderBoardResponseDto
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ namespace SeaHaven.Services.Interfaces
|
|||
Task<WorkOrderBoardResponseDto> GetBoardAsync(
|
||||
WorkOrderBoardQueryDto query,
|
||||
ClaimsPrincipal user,
|
||||
string? currentUserId);
|
||||
string? currentUserId,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<DispatcherLookupDto>> GetDispatcherLookupsAsync();
|
||||
Task<WorkOrderBoardRowDto?> GetBoardRowAsync(int workOrderId, ClaimsPrincipal user);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,8 +141,7 @@ public class WorkOrderBoardSearchCountsTests
|
|||
Assert.Equal(2, result.ScheduledTotalBeforeSearch);
|
||||
Assert.Single(result.ScheduledRows);
|
||||
Assert.Equal("BK5", result.ScheduledRows[0].SiteCode);
|
||||
Assert.Single(result.UnscheduledRows);
|
||||
Assert.Equal("BK5", result.UnscheduledRows[0].SiteCode);
|
||||
Assert.Empty(result.UnscheduledRows);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -242,7 +241,23 @@ public class WorkOrderAdvancedSearchServiceTests
|
|||
PageSize = 500
|
||||
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
|
||||
|
||||
Assert.Equal(100, result.PageSize);
|
||||
Assert.Equal(200, result.PageSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchAsync_AcceptsPageSize200()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var dataService = new WorkOrderAdvancedSearchDataService(context);
|
||||
var service = new SeaHaven.Services.Implementation.WorkOrderAdvancedSearchService(dataService, WorkOrderAccountTestHelpers.Resolver(context));
|
||||
|
||||
var result = await service.SearchAsync(new SeaHaven.Services.DTOs.WorkOrderAdvancedSearchQueryDto
|
||||
{
|
||||
DatePreset = SeaHaven.Services.Helpers.WorkOrderAdvancedSearchDatePreset.ThisWeek,
|
||||
PageSize = 200
|
||||
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
|
||||
|
||||
Assert.Equal(200, result.PageSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ public class WorkOrderBoardDataServiceTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBoardRows_ReturnsScheduledInWeekAndUnscheduled()
|
||||
public async Task GetBoardRows_ReturnsScheduledInWeekAndOmitsUnscheduledDump()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var weekStart = new DateOnly(2026, 6, 22);
|
||||
|
|
@ -74,11 +74,56 @@ public class WorkOrderBoardDataServiceTests
|
|||
Assert.Single(result.ScheduledRows);
|
||||
Assert.Equal(1, result.ScheduledRows[0].Id);
|
||||
Assert.Equal("Acme HVAC", result.ScheduledRows[0].VendorName);
|
||||
Assert.Single(result.UnscheduledRows);
|
||||
Assert.Equal(2, result.UnscheduledRows[0].Id);
|
||||
Assert.Empty(result.UnscheduledRows);
|
||||
Assert.Equal(1, result.ScheduledTotalBeforeSearch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBoardRows_DifferentWeeksReturnDifferentScheduledSets()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var weekA = new DateOnly(2026, 6, 22);
|
||||
var weekB = new DateOnly(2026, 6, 29);
|
||||
|
||||
context.workOrders.AddRange(
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
InternalWONumber = "10000000001",
|
||||
ScheduledDate = new DateTime(2026, 6, 23),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled
|
||||
},
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 2,
|
||||
InternalWONumber = "10000000002",
|
||||
ScheduledDate = new DateTime(2026, 6, 30),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled
|
||||
},
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 3,
|
||||
InternalWONumber = "10000000003",
|
||||
ScheduledDate = null,
|
||||
LifecycleStatus = LifecycleStatus.Incomplete
|
||||
});
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var service = new WorkOrderBoardDataService(context);
|
||||
var resultA = await service.GetBoardRowsAsync(new WorkOrderBoardQuery(
|
||||
weekA, weekA.AddDays(4), null, false, null, false, null, null));
|
||||
var resultB = await service.GetBoardRowsAsync(new WorkOrderBoardQuery(
|
||||
weekB, weekB.AddDays(4), null, false, null, false, null, null));
|
||||
|
||||
Assert.Equal(1, resultA.ScheduledRows[0].Id);
|
||||
Assert.Equal(2, resultB.ScheduledRows[0].Id);
|
||||
Assert.Empty(resultA.UnscheduledRows);
|
||||
Assert.Empty(resultB.UnscheduledRows);
|
||||
Assert.Equal(1, resultA.ScheduledTotalBeforeSearch);
|
||||
Assert.Equal(1, resultB.ScheduledTotalBeforeSearch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBoardRows_FiltersByDispatcher()
|
||||
{
|
||||
|
|
@ -374,6 +419,122 @@ public class WorkOrderBoardPendingUpliftTests
|
|||
Assert.Single(result.ScheduledRows);
|
||||
Assert.Equal(0, result.ScheduledRows[0].PendingUpliftCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBoardRows_PendingUpliftOnPrimaryAndDispatchWorkOrderLink_DoesNotDoubleCount()
|
||||
{
|
||||
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.DispatchWorkOrders.Add(new DispatchWorkOrder
|
||||
{
|
||||
DispatchId = 10,
|
||||
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 = "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);
|
||||
Assert.True(result.ScheduledRows[0].HasUplift);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBoardRows_ScheduledDateWithTimeOnWeekEnd_IsIncluded()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var weekStart = new DateOnly(2026, 6, 22);
|
||||
var weekEnd = weekStart.AddDays(4);
|
||||
|
||||
context.workOrders.Add(new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
InternalWONumber = "10000000001",
|
||||
ScheduledDate = weekEnd.ToDateTime(new TimeOnly(23, 59, 59)),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var service = new WorkOrderBoardDataService(context);
|
||||
var result = await service.GetBoardRowsAsync(new WorkOrderBoardQuery(
|
||||
weekStart,
|
||||
weekEnd,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null));
|
||||
|
||||
Assert.Single(result.ScheduledRows);
|
||||
Assert.Equal(1, result.ScheduledRows[0].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBoardRows_CanceledToken_ThrowsOperationCanceled()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var weekStart = new DateOnly(2026, 6, 22);
|
||||
context.workOrders.Add(new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
InternalWONumber = "10000000001",
|
||||
ScheduledDate = new DateTime(2026, 6, 23),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
var service = new WorkOrderBoardDataService(context);
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
|
||||
service.GetBoardRowsAsync(
|
||||
new WorkOrderBoardQuery(
|
||||
weekStart,
|
||||
weekStart.AddDays(4),
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null),
|
||||
cts.Token));
|
||||
}
|
||||
}
|
||||
|
||||
public class WorkOrderBoardServiceTests
|
||||
|
|
@ -460,4 +621,32 @@ public class WorkOrderBoardServiceTests
|
|||
WeekEnd = new DateOnly(2026, 6, 15)
|
||||
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBoardAsync_CanceledToken_ThrowsOperationCanceled()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
context.workOrders.Add(new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
InternalWONumber = "10000000001",
|
||||
ScheduledDate = new DateTime(2026, 6, 23),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var boardService = new WorkOrderBoardService(
|
||||
new WorkOrderBoardDataService(context),
|
||||
WorkOrderAccountTestHelpers.Resolver(context));
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
|
||||
boardService.GetBoardAsync(
|
||||
new WorkOrderBoardQueryDto { WeekStart = new DateOnly(2026, 6, 22) },
|
||||
WorkOrderAccountTestHelpers.OrgWideAdmin(),
|
||||
null,
|
||||
cts.Token));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue