fix(work-orders): use dedicated overdue query flag

Stop treating WorkOrderType.Other as an overdue sentinel. Board and advanced search now accept overdue=true while types=Other filters real Other rows; combining both uses OR.
This commit is contained in:
Arthur Bassi 2026-07-21 10:36:21 -03:00
parent 188b4f0c58
commit f6c7c1ec7f
13 changed files with 134 additions and 12 deletions

View file

@ -112,6 +112,7 @@ namespace Api.SeaHavenIndustries.Controllers
[FromQuery] List<string>? dispatchers = null,
[FromQuery] bool myWorkOrders = false,
[FromQuery] List<WorkOrderType>? types = null,
[FromQuery] bool overdue = false,
[FromQuery] string? search = null)
{
try
@ -124,6 +125,7 @@ namespace Api.SeaHavenIndustries.Controllers
Dispatchers = dispatchers,
MyWorkOrders = myWorkOrders,
Types = types,
Overdue = overdue,
Search = search
};

View file

@ -38,22 +38,25 @@ namespace SeaHaven.DataServices.Helpers
}
/// <summary>
/// FE sends <see cref="WorkOrderType.Other"/> (99) as the overdue/isPastDue sentinel — not a real type filter.
/// Filters by real <paramref name="types"/> (including <see cref="WorkOrderType.Other"/>)
/// and/or past-due rows when <paramref name="overdue"/> is true.
/// Types and overdue are combined with OR when both are present.
/// </summary>
public static IQueryable<WorkOrder> ApplyTypeAndOverdueFilter(
IQueryable<WorkOrder> query,
IReadOnlyList<WorkOrderType>? types,
bool overdue = false,
DateTime? utcNow = null)
{
if (types == null || types.Count == 0)
var hasTypes = types != null && types.Count > 0;
if (!hasTypes && !overdue)
return query;
var today = (utcNow ?? DateTime.UtcNow).Date;
var includeOverdue = types.Contains(WorkOrderType.Other);
var typeFilters = types.Where(t => t != WorkOrderType.Other).ToList();
var typeFilters = hasTypes ? types!.ToList() : new List<WorkOrderType>();
// Keep past-due predicate inline — EF cannot translate local method calls.
if (includeOverdue && typeFilters.Count == 0)
if (overdue && !hasTypes)
{
return query.Where(w =>
w.ScheduledDate != null
@ -62,7 +65,7 @@ namespace SeaHaven.DataServices.Helpers
&& w.LifecycleStatus != LifecycleStatus.Canceled);
}
if (includeOverdue)
if (overdue)
{
return query.Where(w =>
(w.ScheduledDate != null
@ -77,8 +80,9 @@ namespace SeaHaven.DataServices.Helpers
public static IQueryable<WorkOrder> ApplyTypeFilter(
IQueryable<WorkOrder> query,
IReadOnlyList<WorkOrderType>? types)
=> ApplyTypeAndOverdueFilter(query, types);
IReadOnlyList<WorkOrderType>? types,
bool overdue = false)
=> ApplyTypeAndOverdueFilter(query, types, overdue);
public static IQueryable<WorkOrder> ApplySiteFilter(
IQueryable<WorkOrder> query,

View file

@ -19,7 +19,7 @@ namespace SeaHaven.DataServices.Implementation
var baseQuery = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking());
baseQuery = WorkOrderBoardQueryFilters.ApplyDateRangeFilter(baseQuery, query.DateFrom, query.DateTo);
baseQuery = WorkOrderBoardQueryFilters.ApplySiteFilter(baseQuery, query.Sites);
baseQuery = WorkOrderBoardQueryFilters.ApplyTypeFilter(baseQuery, query.Types);
baseQuery = WorkOrderBoardQueryFilters.ApplyTypeFilter(baseQuery, query.Types, query.Overdue);
baseQuery = WorkOrderBoardQueryFilters.ApplyDispatcherFilter(
baseQuery, query.Dispatchers, query.MyWorkOrders, query.CurrentUserId);
baseQuery = WorkOrderBoardQueryFilters.ApplyStatusFilter(baseQuery, query.Statuses);

View file

@ -23,7 +23,7 @@ namespace SeaHaven.DataServices.Implementation
var baseQuery = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking());
baseQuery = WorkOrderBoardQueryFilters.ApplyDispatcherFilter(
baseQuery, query.Dispatchers, query.MyWorkOrders, query.CurrentUserId);
baseQuery = WorkOrderBoardQueryFilters.ApplyTypeFilter(baseQuery, query.Types);
baseQuery = WorkOrderBoardQueryFilters.ApplyTypeFilter(baseQuery, query.Types, query.Overdue);
var scheduledBase = baseQuery.Where(w =>
(w.ScheduledDate != null

View file

@ -9,6 +9,7 @@ namespace SeaHaven.DataServices.Interfaces
DateOnly DateTo,
IReadOnlyList<string>? Sites,
IReadOnlyList<WorkOrderType>? Types,
bool Overdue,
IReadOnlyList<string>? Dispatchers,
IReadOnlyList<LifecycleStatus>? Statuses,
IReadOnlyList<string>? PmTypes,

View file

@ -8,6 +8,7 @@ namespace SeaHaven.DataServices.Interfaces
IReadOnlyList<string>? Dispatchers,
bool MyWorkOrders,
IReadOnlyList<WorkOrderType>? Types,
bool Overdue,
string? Search,
string? CurrentUserId);

View file

@ -10,6 +10,8 @@ namespace SeaHaven.Services.DTOs
public List<string>? Dispatchers { get; set; }
public bool MyWorkOrders { get; set; }
public List<WorkOrderType>? Types { get; set; }
/// <summary>When true, include past-due (isPastDue) rows. Combined with Types via OR.</summary>
public bool Overdue { get; set; }
public string? Search { get; set; }
}

View file

@ -51,6 +51,8 @@ namespace SeaHaven.Services.DTOs
public DateOnly? DateTo { get; set; }
public List<string>? Sites { get; set; }
public List<WorkOrderType>? Types { get; set; }
/// <summary>When true, include past-due (isPastDue) rows. Combined with Types via OR.</summary>
public bool Overdue { get; set; }
public List<string>? Dispatchers { get; set; }
public List<LifecycleStatus>? Statuses { get; set; }
public List<string>? PmTypes { get; set; }

View file

@ -38,6 +38,7 @@ namespace SeaHaven.Services.Implementation
dateRange.To,
query.Sites,
query.Types,
query.Overdue,
query.Dispatchers,
query.Statuses,
query.PmTypes,

View file

@ -30,6 +30,7 @@ namespace SeaHaven.Services.Implementation
query.Dispatchers,
query.MyWorkOrders,
query.Types,
query.Overdue,
query.Search,
currentUserId);

View file

@ -134,6 +134,7 @@ public class WorkOrderBoardSearchCountsTests
null,
false,
null,
false,
"BK5",
null));
@ -288,7 +289,7 @@ public class WorkOrderAdvancedSearchServiceTests
}
[Fact]
public async Task SearchAsync_OverdueTypeSentinel_FiltersPastDueOnly()
public async Task SearchAsync_TypesOther_ReturnsOtherRowsNotPastDueSentinel()
{
await using var context = CreateContext();
var today = DateTime.UtcNow.Date;
@ -324,11 +325,114 @@ public class WorkOrderAdvancedSearchServiceTests
Types = new List<WorkOrderType> { WorkOrderType.Other }
}, null);
Assert.Equal(1, result.TotalCount);
Assert.Equal(2, result.Items.First().Id);
Assert.Equal(WorkOrderType.Other, result.Items.First().WorkOrderType);
Assert.False(result.Items.First().IsPastDue);
}
[Fact]
public async Task SearchAsync_OverdueTrue_ReturnsPastDueRowsOnly()
{
await using var context = CreateContext();
var today = DateTime.UtcNow.Date;
context.workOrders.AddRange(
new WorkOrder
{
Id = 1,
WorkOrderType = WorkOrderType.PM,
ScheduledDate = today.AddDays(-3),
LifecycleStatus = LifecycleStatus.Scheduled,
InternalWONumber = "10000000001"
},
new WorkOrder
{
Id = 2,
WorkOrderType = WorkOrderType.Other,
ScheduledDate = today.AddDays(3),
LifecycleStatus = LifecycleStatus.Scheduled,
InternalWONumber = "10000000002"
},
new WorkOrder
{
Id = 3,
WorkOrderType = WorkOrderType.Reactive,
ScheduledDate = today.AddDays(1),
LifecycleStatus = LifecycleStatus.Scheduled,
InternalWONumber = "10000000003"
});
await context.SaveChangesAsync();
var dataService = new WorkOrderAdvancedSearchDataService(context);
var service = new SeaHaven.Services.Implementation.WorkOrderAdvancedSearchService(dataService);
var result = await service.SearchAsync(new SeaHaven.Services.DTOs.WorkOrderAdvancedSearchQueryDto
{
DatePreset = SeaHaven.Services.Helpers.WorkOrderAdvancedSearchDatePreset.Custom,
DateFrom = DateOnly.FromDateTime(today.AddMonths(-1)),
DateTo = DateOnly.FromDateTime(today.AddMonths(1)),
Overdue = true
}, null);
Assert.Equal(1, result.TotalCount);
Assert.Equal(1, result.Items.First().Id);
Assert.True(result.Items.First().IsPastDue);
}
[Fact]
public async Task SearchAsync_OtherPlusOverdue_ReturnsUnion()
{
await using var context = CreateContext();
var today = DateTime.UtcNow.Date;
context.workOrders.AddRange(
new WorkOrder
{
Id = 1,
WorkOrderType = WorkOrderType.PM,
ScheduledDate = today.AddDays(-3),
LifecycleStatus = LifecycleStatus.Scheduled,
InternalWONumber = "10000000001"
},
new WorkOrder
{
Id = 2,
WorkOrderType = WorkOrderType.Other,
ScheduledDate = today.AddDays(3),
LifecycleStatus = LifecycleStatus.Scheduled,
InternalWONumber = "10000000002"
},
new WorkOrder
{
Id = 3,
WorkOrderType = WorkOrderType.Reactive,
ScheduledDate = today.AddDays(1),
LifecycleStatus = LifecycleStatus.Scheduled,
InternalWONumber = "10000000003"
});
await context.SaveChangesAsync();
var dataService = new WorkOrderAdvancedSearchDataService(context);
var service = new SeaHaven.Services.Implementation.WorkOrderAdvancedSearchService(dataService);
var result = await service.SearchAsync(new SeaHaven.Services.DTOs.WorkOrderAdvancedSearchQueryDto
{
DatePreset = SeaHaven.Services.Helpers.WorkOrderAdvancedSearchDatePreset.Custom,
DateFrom = DateOnly.FromDateTime(today.AddMonths(-1)),
DateTo = DateOnly.FromDateTime(today.AddMonths(1)),
Types = new List<WorkOrderType> { WorkOrderType.Other },
Overdue = true
}, null);
Assert.Equal(2, result.TotalCount);
Assert.Contains(result.Items, i => i.Id == 1 && i.IsPastDue);
Assert.Contains(result.Items, i => i.Id == 2 && i.WorkOrderType == WorkOrderType.Other);
Assert.DoesNotContain(result.Items, i => i.Id == 3);
}
[Fact]
public async Task SearchAsync_IncludesUnscheduledRows()
{

View file

@ -67,6 +67,7 @@ public class WorkOrderBoardDataServiceTests
null,
false,
null,
false,
null,
null));
@ -109,6 +110,7 @@ public class WorkOrderBoardDataServiceTests
new[] { "user-a" },
false,
null,
false,
null,
null));
@ -147,6 +149,7 @@ public class WorkOrderBoardDataServiceTests
null,
true,
null,
false,
null,
"me"));

View file

@ -47,7 +47,8 @@ Advanced search cross-week paginado.
| `datePreset` | enum | `thisWeek`, `lastWeek`, `thisMonth`, `last3Months`, `nextWeek`, `nextMonth`, `custom` |
| `dateFrom` / `dateTo` | date | Obrigatórios se `datePreset=custom` |
| `sites` | string[] | `SiteCode` |
| `types` | WorkOrderType[] | Tipo WO |
| `types` | WorkOrderType[] | Tipo WO (inclui `Other` como tipo real) |
| `overdue` | bool | Past-due (`isPastDue`); default `false`; OR com `types` |
| `dispatchers` | string[] | GUIDs + `__unassigned__` |
| `statuses` | LifecycleStatus[] | Status operacional |
| `pmTypes` | string[] | Match em `Trade`/`Problem` (contains, case-insensitive) |